first commit 2
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 공통 파일 (관리자용)
|
||||
*/
|
||||
|
||||
define('G5_IS_ADMIN', true);
|
||||
include_once('../../common.php');
|
||||
include_once(G5_ADMIN_PATH . '/admin.lib.php');
|
||||
|
||||
// 💡 [개선] 공통 함수를 별도 파일로 분리하여 관리합니다.
|
||||
include_once(__DIR__ . '/functions.php');
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
// AJAX 요청 등으로 직접 접근했을 때, 그누보드 환경을 로드합니다.
|
||||
include_once('../../../common.php');
|
||||
}
|
||||
|
||||
// 💡 [개선] 공통 함수를 별도 파일로 분리하여 관리합니다.
|
||||
include_once(__DIR__ . '/functions.php');
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 메뉴
|
||||
*/
|
||||
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// 메뉴 구조: array('메뉴코드', '메뉴명', '링크', '메뉴 ID', '아이콘 클래스 (옵션)')
|
||||
$menu['menu850'] = array(
|
||||
// 💡 [개선] 메뉴 그룹 대표 아이콘 추가 (예: fa-comments)
|
||||
array('850000', '상담관리', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_manage', 'fa-comments'),
|
||||
array('850100', '대시보드', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_dashboard'),
|
||||
array('850200', '예약 현황', G5_ADMIN_URL . '/consultant_manage/reservations.php', 'consultant_reservations'),
|
||||
array('850300', '빠른 스케줄 관리', G5_ADMIN_URL . '/consultant_manage/schedule_generate.php', 'consultant_schedule_quick'),
|
||||
array('850400', '통계 분석', G5_ADMIN_URL . '/consultant_manage/statistics.php', 'consultant_statistics'),
|
||||
// 💡 [개선] 설정 관련 메뉴를 '환경설정' 그룹으로 통합
|
||||
array('850500', '팝업 샘플', G5_ADMIN_URL . '/consultant_manage/sample_page.php', 'consultant_sample'),
|
||||
array('850600', '환경설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings_group', 'fa-cogs'),
|
||||
array('850610', '기본/운영 설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings'),
|
||||
array('850615', '리소스(상담사) 관리', G5_ADMIN_URL . '/consultant_manage/resources.php', 'consultant_resources'), // 💡 [추가] 리소스 관리 메뉴
|
||||
array('850620', '알림 템플릿', G5_ADMIN_URL . '/consultant_manage/templates.php', 'consultant_templates'),
|
||||
array('850630', '시스템 로그', G5_ADMIN_URL . '/consultant_manage/log_view.php', 'consultant_log_view'),
|
||||
array('850640', '설치/업데이트', G5_ADMIN_URL . '/consultant_manage/install.php', 'consultant_install'),
|
||||
// array('850900', '시스템 테스트', G5_ADMIN_URL . '/consultant_manage/test_system.php', 'consultant_test') // 개발용 메뉴는 주석 처리
|
||||
);
|
||||
?>
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* 스케줄 생성 AJAX 처리
|
||||
*/
|
||||
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
die(json_encode(['success' => false, 'message' => '권한이 없습니다.']));
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
die(json_encode(['success' => false, 'message' => '상담 예약 시스템이 설치되지 않았습니다.']));
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$year = (int) ($_POST['year'] ?? date('Y'));
|
||||
$month = (int) ($_POST['month'] ?? date('n'));
|
||||
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
|
||||
try {
|
||||
$generator = new ScheduleGenerator();
|
||||
|
||||
switch ($action) {
|
||||
case 'generate_month':
|
||||
$result = $generator->generateMonth($year, $month);
|
||||
if ($result) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => "{$year}년 {$month}월 스케줄이 생성되었습니다."
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '스케줄 생성에 실패했습니다.'
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'check_conflicts':
|
||||
$conflicts = $generator->checkScheduleConflicts($year, $month);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'conflicts' => $conflicts,
|
||||
'message' => count($conflicts) > 0 ?
|
||||
count($conflicts) . '개의 충돌이 발견되었습니다.' :
|
||||
'충돌이 없습니다.'
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'regenerate_next_month':
|
||||
$nextMonth = date('n') == 12 ? 1 : date('n') + 1;
|
||||
$nextYear = date('n') == 12 ? date('Y') + 1 : date('Y');
|
||||
|
||||
$result = $generator->generateMonth($nextYear, $nextMonth);
|
||||
if ($result) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => "다음 달({$nextYear}년 {$nextMonth}월) 스케줄이 재생성되었습니다."
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '다음 달 스케줄 재생성에 실패했습니다.'
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['success' => false, 'message' => '잘못된 요청입니다.']);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '오류가 발생했습니다: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -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}' 파일에 내용을 쓸 수 없습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 이 파일은 상담 예약 관련 팝업들을 한번에 쉽게 포함하기 위해 사용됩니다.
|
||||
|
||||
// 현재 파일의 경로를 기준으로 팝업 파일들의 경로를 정의합니다.
|
||||
$consultant_components_path = dirname(__FILE__);
|
||||
|
||||
// 상담 예약 신청 팝업 포함
|
||||
include_once($consultant_components_path . '/reservation_popup.php');
|
||||
|
||||
// 예약 확인/취소 팝업 포함
|
||||
include_once($consultant_components_path . '/reservation_check.php');
|
||||
?>
|
||||
@@ -0,0 +1,401 @@
|
||||
<?php
|
||||
if (isset($_POST['action'])) {
|
||||
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
// 예약 조회
|
||||
if ($action === 'find_reservations') {
|
||||
$customer_name = trim($_POST['customer_name'] ?? '');
|
||||
$customer_phone = trim($_POST['customer_phone'] ?? '');
|
||||
|
||||
if (!$customer_name || !$customer_phone) {
|
||||
echo json_encode(['success' => false, 'message' => '이름과 연락처를 모두 입력해주세요.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 💡 [수정] 리소스 정보를 함께 조회하기 위해 LEFT JOIN 추가
|
||||
$sql = " SELECT r.*, res.name as resource_name
|
||||
FROM consultant_reservations r
|
||||
LEFT JOIN consultant_resources res ON r.resource_id = res.id
|
||||
WHERE r.customer_name = '" . sql_real_escape_string($customer_name) . "'
|
||||
AND r.customer_phone = '" . sql_real_escape_string($customer_phone) . "'
|
||||
AND r.is_deleted = 0
|
||||
ORDER BY r.reservation_date DESC, r.reservation_time DESC ";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$reservations = [];
|
||||
$cancel_deadline_hours = (int)consultant_get_config('cancel_deadline_hours', 24);
|
||||
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$reservation_timestamp = strtotime($row['reservation_date'] . ' ' . $row['reservation_time']);
|
||||
$cancellable_until = $reservation_timestamp - ($cancel_deadline_hours * 3600);
|
||||
|
||||
$row['is_cancellable'] = (time() < $cancellable_until && in_array($row['status'], ['payment_pending', 'reserved']));
|
||||
$reservations[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $reservations]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 예약 취소
|
||||
if ($action === 'cancel_reservation') {
|
||||
$reservation_id = (int)($_POST['reservation_id'] ?? 0);
|
||||
$customer_name = trim($_POST['customer_name'] ?? '');
|
||||
$customer_phone = trim($_POST['customer_phone'] ?? '');
|
||||
|
||||
if (!$reservation_id || !$customer_name || !$customer_phone) {
|
||||
echo json_encode(['success' => false, 'message' => '필수 정보가 누락되었습니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = " SELECT * FROM consultant_reservations
|
||||
WHERE id = '{$reservation_id}'
|
||||
AND customer_name = '" . sql_real_escape_string($customer_name) . "'
|
||||
AND customer_phone = '" . sql_real_escape_string($customer_phone) . "'
|
||||
AND is_deleted = 0 ";
|
||||
$reservation = sql_fetch($sql);
|
||||
|
||||
if (!$reservation) {
|
||||
echo json_encode(['success' => false, 'message' => '예약 정보를 찾을 수 없거나, 입력하신 정보와 일치하지 않습니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cancel_deadline_hours = (int)consultant_get_config('cancel_deadline_hours', 24);
|
||||
$reservation_timestamp = strtotime($reservation['reservation_date'] . ' ' . $reservation['reservation_time']);
|
||||
$cancellable_until = $reservation_timestamp - ($cancel_deadline_hours * 3600);
|
||||
|
||||
if (time() >= $cancellable_until) {
|
||||
echo json_encode(['success' => false, 'message' => '예약 취소 가능 시간이 지났습니다. 관리자에게 문의해주세요.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!in_array($reservation['status'], ['payment_pending', 'reserved'])) {
|
||||
echo json_encode(['success' => false, 'message' => '이미 처리되었거나 취소된 예약입니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql_update = " UPDATE consultant_reservations
|
||||
SET status = 'cancelled', updated_at = NOW()
|
||||
WHERE id = '{$reservation_id}' ";
|
||||
|
||||
if (sql_query($sql_update)) {
|
||||
consultant_log("고객 예약 취소: ID {$reservation_id} (고객: {$customer_name})");
|
||||
echo json_encode(['success' => true, 'message' => '예약이 성공적으로 취소되었습니다.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => '예약 취소 중 오류가 발생했습니다. 다시 시도해주세요.']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
|
||||
// 💡 [추가] 상담 유형 한글 이름을 JavaScript에서 사용하기 위해 배열을 정의합니다.
|
||||
$consultant_types_map = json_decode(consultant_get_config('consultation_types', '{"onsite":"현장 상담"}'), true);
|
||||
if (!is_array($consultant_types_map)) {
|
||||
$consultant_types_map = ['onsite' => '현장 상담']; // 파싱 실패 시 기본값
|
||||
}
|
||||
|
||||
$ajax_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_check.php';
|
||||
?>
|
||||
|
||||
<!-- 예약 확인/취소 팝업 -->
|
||||
<div id="reservation-check-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="팝업 닫기">×</button>
|
||||
</div>
|
||||
<div class="reservation-modal-body">
|
||||
<div class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [개선] 조회 폼과 결과 화면을 분리합니다. -->
|
||||
<div id="reservation-check-view-form">
|
||||
<div class="check-form-wrap">
|
||||
<p>예약 시 입력하신 이름과 연락처로 예약 내역을 조회할 수 있습니다.</p>
|
||||
<form id="reservation-check-form">
|
||||
<div class="form-group">
|
||||
<label for="check_customer_name">예약자명</label>
|
||||
<input type="text" id="check_customer_name" name="customer_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="check_customer_phone">연락처</label>
|
||||
<input type="tel" id="check_customer_phone" name="customer_phone" class="form-control" placeholder="010-1234-5678" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">예약 조회</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="reservation-check-view-results" style="display: none;">
|
||||
<div class="results-header">
|
||||
<h3>조회된 예약 내역</h3>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="back-to-search-btn">새로 조회</button>
|
||||
</div>
|
||||
<div id="reservation-results">
|
||||
<!-- 검색 결과가 여기에 표시됩니다. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 💡 [개선] 예약 신청 팝업과 스타일을 통일합니다. */
|
||||
.check-form-wrap { background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; font-weight: 500; color: #495057; margin-bottom: 8px; }
|
||||
.form-control { width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 16px; box-sizing: border-box; } /* iOS 줌 방지 */
|
||||
.form-control:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); }
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s ease;
|
||||
/* 💡 [수정] 버튼 내 텍스트가 잘려보이는 현상을 해결합니다. */
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-primary:hover { background: #0056b3; }
|
||||
#reservation-results { margin-top: 20px; }
|
||||
.reservation-item { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
|
||||
.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-reserved { background: #d4edda; color: #155724; }
|
||||
/* 💡 [개선] 예약 상세 정보 스타일 */
|
||||
.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;
|
||||
}
|
||||
.reservation-actions .btn-danger {
|
||||
width: auto; /* 버튼이 전체 너비를 차지하지 않도록 */
|
||||
display: inline-block;
|
||||
}
|
||||
.cancel-notice { font-size: 13px !important; color: #666 !important; margin-top: 15px; text-align: right; padding: 10px; background-color: #fff; border-radius: 4px;}
|
||||
/* 💡 [개선] 예약 조회 결과 헤더 스타일 */
|
||||
.results-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #eee; }
|
||||
.results-header h3 { margin: 0; font-size: 18px; flex-grow: 1; } /* 제목이 남는 공간을 모두 차지하도록 설정 */
|
||||
.results-header .btn { width: auto; flex-shrink: 0; } /* 헤더 안의 버튼은 내용만큼만 너비를 갖도록 설정 */
|
||||
.btn-sm { padding: 5px 10px; font-size: 12px; }
|
||||
.status-payment_pending { background: #fff3cd; color: #856404; }
|
||||
.status-cancelled { background: #f8d7da; color: #721c24; }
|
||||
.status-completed { background: #cce5ff; color: #004085; }
|
||||
.btn-danger { background: #dc3545; color: white; }
|
||||
.no-results { text-align: center; color: #666; padding: 40px 0; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const ReservationCheckPopup = {
|
||||
elements: {},
|
||||
// 💡 [추가] PHP에서 전달받은 상담 유형 맵
|
||||
consultantTypes: <?php echo json_encode($consultant_types_map, JSON_UNESCAPED_UNICODE); ?>,
|
||||
state: {
|
||||
name: '',
|
||||
phone: ''
|
||||
},
|
||||
config: {
|
||||
ajaxUrl: '<?php echo $ajax_url; ?>'
|
||||
},
|
||||
|
||||
init() {
|
||||
this.elements.popup = document.getElementById('reservation-check-popup');
|
||||
if (!this.elements.popup) return;
|
||||
|
||||
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
|
||||
this.elements.form = this.elements.popup.querySelector('#reservation-check-form');
|
||||
this.elements.resultsContainer = this.elements.popup.querySelector('#reservation-results');
|
||||
|
||||
// 💡 [추가] 화면 전환용 요소
|
||||
this.elements.formView = this.elements.popup.querySelector('#reservation-check-view-form');
|
||||
this.elements.resultsView = this.elements.popup.querySelector('#reservation-check-view-results');
|
||||
this.elements.backBtn = this.elements.popup.querySelector('#back-to-search-btn');
|
||||
|
||||
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.form) {
|
||||
this.elements.form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
// 💡 [개선] 조회 버튼을 누를 때만 state를 업데이트하고, 그 state를 기반으로 조회합니다.
|
||||
this.state.name = this.elements.form.querySelector('#check_customer_name').value;
|
||||
this.state.phone = this.elements.form.querySelector('#check_customer_phone').value;
|
||||
this.findReservations(); // state에 저장된 정보로 조회
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.backBtn) {
|
||||
this.elements.backBtn.addEventListener('click', () => {
|
||||
this.elements.resultsView.style.display = 'none';
|
||||
this.elements.formView.style.display = 'block';
|
||||
this.elements.resultsContainer.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
},
|
||||
|
||||
close() {
|
||||
if (!this.elements.popup) return;
|
||||
this.elements.popup.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
if (this.elements.form) this.elements.form.reset();
|
||||
|
||||
// 💡 [추가] 팝업을 닫을 때 화면 상태를 초기화합니다.
|
||||
if (this.elements.resultsContainer) this.elements.resultsContainer.innerHTML = '';
|
||||
if (this.elements.resultsView) this.elements.resultsView.style.display = 'none';
|
||||
if (this.elements.formView) this.elements.formView.style.display = 'block';
|
||||
},
|
||||
|
||||
async findReservations() {
|
||||
this.showLoading();
|
||||
|
||||
// 💡 [개선] state에 저장된 정보로 FormData를 생성하여 일관성을 유지합니다.
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'find_reservations');
|
||||
formData.append('customer_name', this.state.name);
|
||||
formData.append('customer_phone', this.state.phone);
|
||||
|
||||
try {
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// 💡 [추가] 조회 성공 시 화면을 전환합니다.
|
||||
this.elements.formView.style.display = 'none';
|
||||
this.elements.resultsView.style.display = 'block';
|
||||
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;
|
||||
}
|
||||
|
||||
// 💡 [수정] h3 태그는 정적 HTML로 이동했으므로 제거합니다.
|
||||
let html = '';
|
||||
const statusLabels = {
|
||||
'payment_pending': '입금대기', 'reserved': '예약확정',
|
||||
'completed': '상담완료', 'cancelled': '예약취소'
|
||||
};
|
||||
|
||||
reservations.forEach(res => {
|
||||
const date = new Date(res.reservation_date + ' ' + res.reservation_time);
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit' };
|
||||
const formattedDate = date.toLocaleDateString('ko-KR', options);
|
||||
|
||||
// 💡 [개선] 상담 유형의 한글 이름을 맵에서 찾아옵니다.
|
||||
const consultationTypeName = this.consultantTypes[res.consultation_type] || res.consultation_type;
|
||||
|
||||
// 💡 [추가] 리소스 이름 표시
|
||||
const resourceName = res.resource_name ? res.resource_name : '미지정 (빠른 배정)';
|
||||
|
||||
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}">${statusLabels[res.status] || res.status}</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">${consultationTypeName}</span></div>
|
||||
<div class="detail-item"><span class="label">담당/공간:</span><span class="value">${resourceName}</span></div>
|
||||
</div>
|
||||
<div class="reservation-actions">`;
|
||||
|
||||
if (res.is_cancellable) {
|
||||
html += `<button type="button" class="btn 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_reservation');
|
||||
formData.append('reservation_id', reservationId);
|
||||
formData.append('customer_name', this.state.name);
|
||||
formData.append('customer_phone', this.state.phone);
|
||||
|
||||
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.findReservations(); // 💡 [수정] await를 추가하여 목록 새로고침이 완료될 때까지 기다립니다.
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("취소 오류:", error);
|
||||
alert('예약 취소 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
// 💡 [제거] findReservations()가 자체적으로 로딩을 숨기므로 중복 호출을 제거합니다.
|
||||
}
|
||||
},
|
||||
|
||||
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', () => ReservationCheckPopup.init());
|
||||
|
||||
function openReservationCheckPopup() {
|
||||
ReservationCheckPopup.open();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,868 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 팝업 UI 및 데이터 처리
|
||||
*/
|
||||
|
||||
// AJAX 요청 처리
|
||||
if (isset($_POST['action'])) {
|
||||
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
|
||||
|
||||
// 월별 예약 가능일 조회
|
||||
if ($action === 'get_month_availability') {
|
||||
$year = (int) ($_POST['year'] ?? 0);
|
||||
$month = (int) ($_POST['month'] ?? 0);
|
||||
|
||||
if ($year && $month) {
|
||||
$start_date = date('Y-m-d', mktime(0, 0, 0, $month, 1, $year));
|
||||
$end_date = date('Y-m-t', strtotime($start_date));
|
||||
|
||||
$max_advance_days = consultant_get_config('max_advance_days', 30);
|
||||
$max_date = date('Y-m-d', strtotime("+" . $max_advance_days . " days"));
|
||||
|
||||
$sql = "SELECT
|
||||
specific_date,
|
||||
SUM(CASE WHEN is_available = 1 AND max_persons > (SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') THEN 1 ELSE 0 END) as available_slots,
|
||||
MAX(CASE WHEN temp_1 = 'holiday' THEN 1 ELSE 0 END) as is_holiday
|
||||
FROM consultant_schedule s
|
||||
WHERE specific_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
GROUP BY specific_date";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$availability = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$is_bookable = true;
|
||||
$reason = '';
|
||||
|
||||
if ($row['is_holiday']) {
|
||||
$is_bookable = false;
|
||||
$reason = 'holiday';
|
||||
} elseif ($row['specific_date'] < date('Y-m-d')) {
|
||||
$is_bookable = false;
|
||||
$reason = 'past_date';
|
||||
} elseif ($row['specific_date'] > $max_date) {
|
||||
$is_bookable = false;
|
||||
$reason = 'too_far';
|
||||
} elseif ($row['available_slots'] == 0) {
|
||||
$is_bookable = false;
|
||||
$reason = 'full';
|
||||
}
|
||||
|
||||
$availability[date('j', strtotime($row['specific_date']))] = [
|
||||
'available' => $is_bookable,
|
||||
'reason' => $reason
|
||||
];
|
||||
}
|
||||
$response = ['success' => true, 'data' => $availability];
|
||||
}
|
||||
}
|
||||
|
||||
// 특정일의 예약 가능 시간 조회
|
||||
if ($action === 'get_time_slots') {
|
||||
$date = preg_replace('/[^0-9\-]/', '', $_POST['date'] ?? '');
|
||||
|
||||
if ($date) {
|
||||
$min_advance_hours = consultant_get_config('min_advance_hours', 24);
|
||||
$min_datetime = date('Y-m-d H:i:s', strtotime("+" . $min_advance_hours . " hours"));
|
||||
|
||||
$sql = "SELECT
|
||||
s.start_time,
|
||||
s.max_persons,
|
||||
(SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') as reserved_count
|
||||
FROM consultant_schedule s
|
||||
WHERE s.specific_date = '{$date}' AND s.is_available = 1
|
||||
ORDER BY s.start_time";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$slots = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$slot_datetime = $date . ' ' . $row['start_time'];
|
||||
$is_too_soon = ($slot_datetime < $min_datetime);
|
||||
|
||||
$available_count = $row['max_persons'] - $row['reserved_count'];
|
||||
$is_full = ($available_count <= 0);
|
||||
|
||||
$slots[] = [
|
||||
'time' => substr($row['start_time'], 0, 5),
|
||||
'available' => !$is_too_soon && !$is_full,
|
||||
'reason' => $is_too_soon ? 'too_soon' : ($is_full ? 'full' : ''),
|
||||
'reserved_count' => (int)$row['reserved_count'],
|
||||
'max_persons' => (int)$row['max_persons']
|
||||
];
|
||||
}
|
||||
$response = ['success' => true, 'data' => $slots];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 💡 [수정] 아래는 팝업의 HTML/CSS/JS 부분입니다.
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
$consultant_installed = is_consultant_installed();
|
||||
|
||||
if($consultant_installed) { // 💡 [추가] 시스템이 설치된 경우에만 팝업을 렌더링합니다.
|
||||
// 현재 날짜 정보
|
||||
$current_year = date('Y');
|
||||
$current_month_num = date('n');
|
||||
|
||||
// 💡 [개선] 설정 값을 DB에서 직접 가져와 일관성을 유지합니다.
|
||||
$consultation_fee = consultant_get_config('consultation_fee', 50000);
|
||||
$account_info = consultant_get_config('account_info', '');
|
||||
$max_advance_days = consultant_get_config('max_advance_days', 30);
|
||||
|
||||
// 💡 [수정] 스킨 URL 및 AJAX 엔드포인트 설정
|
||||
$ajax_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_popup.php';
|
||||
$form_action_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_submit.php';
|
||||
|
||||
// 💡 [추가] 리소스 목록 조회
|
||||
$resources = [];
|
||||
$res_result = sql_query("SELECT id, name FROM consultant_resources WHERE is_active = 1 ORDER BY group_id, name");
|
||||
while ($row = sql_fetch_array($res_result)) {
|
||||
$resources[] = $row;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- 예약 팝업 오버레이 -->
|
||||
<div id="reservation-popup" class="reservation-modal-overlay">
|
||||
<div class="reservation-modal-content">
|
||||
<!-- 팝업 헤더 -->
|
||||
<div class="reservation-modal-header">
|
||||
<h2>상담 예약 신청</h2>
|
||||
<button type="button" class="reservation-modal-close" aria-label="팝업 닫기">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 팝업 본문 -->
|
||||
<div class="reservation-modal-body">
|
||||
<!-- 💡 [개선] 로딩 스피너 추가 -->
|
||||
<div class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- 단계 표시 -->
|
||||
<div class="reservation-steps">
|
||||
<div class="step active" data-step="1">
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-text">날짜 선택</span>
|
||||
</div>
|
||||
<div class="step" data-step="2">
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-text">시간 선택</span>
|
||||
</div>
|
||||
<div class="step" data-step="3">
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-text">정보 입력</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 예약 폼 -->
|
||||
<form id="reservation-form" method="post"
|
||||
action="<?php echo $form_action_url; ?>">
|
||||
<!-- 1단계: 달력 -->
|
||||
<div class="reservation-step-content" data-step="1">
|
||||
<div class="step-description">
|
||||
<h4>📅 상담 날짜를 선택해주세요</h4>
|
||||
<p>최대 <?php echo $max_advance_days; ?>일 후까지 예약 가능합니다.</p>
|
||||
</div>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<button type="button" class="calendar-nav prev-month" aria-label="이전 달">
|
||||
<span>‹</span>
|
||||
</button>
|
||||
<h3 class="calendar-title">
|
||||
<span id="calendar-year"><?php echo $current_year; ?></span>년
|
||||
<span id="calendar-month"><?php echo $current_month_num; ?></span>월
|
||||
</h3>
|
||||
<button type="button" class="calendar-nav next-month" aria-label="다음 달">
|
||||
<span>›</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-weekdays">
|
||||
<div class="weekday">일</div>
|
||||
<div class="weekday">월</div>
|
||||
<div class="weekday">화</div>
|
||||
<div class="weekday">수</div>
|
||||
<div class="weekday">목</div>
|
||||
<div class="weekday">금</div>
|
||||
<div class="weekday">토</div>
|
||||
</div>
|
||||
<div class="calendar-days" id="calendar-days">
|
||||
<!-- 달력 날짜들이 JavaScript로 동적 생성됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
<div class="legend-item"><span class="legend-color available"></span><span>예약 가능</span></div>
|
||||
<!-- 💡 [추가] 휴일/마감 상태를 구분하는 범례 추가 -->
|
||||
<div class="legend-item"><span class="legend-color holiday"></span><span>휴일</span></div>
|
||||
<div class="legend-item"><span class="legend-color full"></span><span>예약 마감</span></div>
|
||||
<div class="legend-item"><span class="legend-color unavailable"></span><span>지난날짜</span></div>
|
||||
<div class="legend-item"><span class="legend-color selected"></span><span>선택</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2단계: 시간 선택 -->
|
||||
<div class="reservation-step-content" data-step="2" style="display: none;">
|
||||
<div class="step-description">
|
||||
<h4>🕐 상담 시간을 선택해주세요</h4>
|
||||
<div class="selected-date-info"><strong>선택된 날짜: <span id="selected-date-display"></span></strong></div>
|
||||
</div>
|
||||
<div class="time-slots-container">
|
||||
<div class="time-slots-grid" id="time-slots-grid">
|
||||
<!-- 시간대들이 JavaScript로 동적 생성됩니다 -->
|
||||
</div>
|
||||
<div class="time-legend">
|
||||
<div class="legend-item"><span class="legend-color time-available"></span><span>예약 가능</span></div>
|
||||
<div class="legend-item"><span class="legend-color time-full"></span><span>예약 마감</span></div>
|
||||
<div class="legend-item"><span class="legend-color time-too-soon"></span><span>예약 임박</span></div>
|
||||
<div class="legend-item"><span class="legend-color time-selected"></span><span>선택</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3단계: 고객 정보 입력 -->
|
||||
<div class="reservation-step-content" data-step="3" style="display: none;">
|
||||
<div class="step-description">
|
||||
<h4>📝 고객 정보를 입력해주세요</h4>
|
||||
</div>
|
||||
|
||||
<div class="reservation-summary">
|
||||
<h5>📋 예약 정보 확인</h5>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-item">
|
||||
<span class="label">📅 예약 날짜:</span>
|
||||
<span id="summary-date">-</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">🕐 예약 시간:</span>
|
||||
<span id="summary-time">-</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">💰 상담 비용:</span>
|
||||
<span><?php echo number_format($consultation_fee); ?>원</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="customer-info-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="customer-name">👤 이름 <span class="required">*</span></label>
|
||||
<input type="text" id="customer-name" name="customer_name" required placeholder="홍길동">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customer-phone">📱 연락처 <span class="required">*</span></label>
|
||||
<input type="tel" id="customer-phone" name="customer_phone" required placeholder="010-1234-5678">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customer-email">📧 이메일 <span class="required">*</span></label>
|
||||
<input type="email" id="customer-email" name="customer_email" required placeholder="example@email.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="consultation-type">🏠 상담 유형</label>
|
||||
<select id="consultation-type" name="consultation_type">
|
||||
<option value="onsite">현장 상담</option>
|
||||
<option value="online">온라인 상담</option>
|
||||
<option value="phone">전화 상담</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 💡 [추가] 담당자/공간 선택 드롭다운 -->
|
||||
<div class="form-group">
|
||||
<label for="consultation-resource">👨⚕️ 담당자/공간 선택</label>
|
||||
<select id="consultation-resource" name="resource_id">
|
||||
<option value="">선택 안 함 (빠른 배정)</option>
|
||||
<?php foreach ($resources as $res): ?>
|
||||
<option value="<?php echo $res['id']; ?>"><?php echo htmlspecialchars($res['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customer-request">📝 요청사항</label>
|
||||
<textarea id="customer-request" name="customer_request" rows="4" placeholder="상담 관련 요청사항이나 문의사항을 입력해주세요."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 숨겨진 필드들 -->
|
||||
<input type="hidden" id="selected-date" name="reservation_date">
|
||||
<input type="hidden" id="selected-time" name="reservation_time">
|
||||
<input type="hidden" name="payment_amount" value="<?php echo $consultation_fee; ?>">
|
||||
<input type="hidden" name="status" value="payment_pending">
|
||||
</div>
|
||||
|
||||
<!-- 네비게이션 버튼 -->
|
||||
<div class="reservation-nav-buttons">
|
||||
<button type="button" class="btn-prev" style="display: none;">← 이전</button>
|
||||
<button type="button" class="btn-next">다음 →</button>
|
||||
<button type="submit" class="btn-submit" style="display: none;">예약 신청</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 예약 팝업 스타일 -->
|
||||
<style>
|
||||
/* 💡 [추가] 로딩 오버레이 스타일 */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.loading-spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
|
||||
/* 팝업 오버레이 */
|
||||
.reservation-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.reservation-modal-overlay.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.reservation-modal-content {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.reservation-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.reservation-modal-header h2 { margin: 0; font-size: 20px; font-weight: 600; color: #333; }
|
||||
.reservation-modal-close {
|
||||
background: none; border: none; font-size: 24px; cursor: pointer; color: #666;
|
||||
padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 50%; transition: all 0.2s ease;
|
||||
}
|
||||
.reservation-modal-close:hover { background: #e9ecef; color: #333; }
|
||||
.reservation-modal-body { padding: 25px; }
|
||||
|
||||
/* 단계 표시 */
|
||||
.reservation-steps { display: flex; justify-content: center; margin-bottom: 30px; position: relative; }
|
||||
.reservation-steps::before {
|
||||
content: ''; position: absolute; top: 15px; left: 25%; right: 25%;
|
||||
height: 2px; background: #e9ecef; z-index: 1;
|
||||
}
|
||||
.step { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 2; background: #fff; padding: 0 15px; }
|
||||
.step-number {
|
||||
width: 30px; height: 30px; border-radius: 50%; background: #e9ecef; color: #6c757d;
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 8px; transition: all 0.3s ease;
|
||||
}
|
||||
.step.active .step-number { background: #007bff; color: #fff; }
|
||||
.step.completed .step-number { background: #28a745; color: #fff; }
|
||||
.step-text { font-size: 12px; color: #6c757d; font-weight: 500; }
|
||||
.step.active .step-text { color: #007bff; font-weight: 600; }
|
||||
|
||||
/* 달력 */
|
||||
.calendar-container { max-width: 100%; }
|
||||
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.calendar-nav {
|
||||
background: none; border: 1px solid #ddd; width: 35px; height: 35px; border-radius: 50%;
|
||||
cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease;
|
||||
}
|
||||
.calendar-nav:hover { background: #f8f9fa; border-color: #007bff; }
|
||||
.calendar-title { margin: 0; font-size: 18px; font-weight: 600; color: #333; }
|
||||
.calendar-grid { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }
|
||||
.calendar-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); background: #f8f9fa; }
|
||||
.weekday { padding: 12px 8px; text-align: center; font-weight: 600; color: #495057; font-size: 14px; border-right: 1px solid #e9ecef; }
|
||||
.weekday:last-child { border-right: none; }
|
||||
.calendar-days { display: grid; grid-template-columns: repeat(7, 1fr); }
|
||||
.calendar-day {
|
||||
aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
|
||||
border-right: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef; cursor: pointer;
|
||||
transition: all 0.2s ease; font-size: 14px; position: relative;
|
||||
}
|
||||
.calendar-day:nth-child(7n) { border-right: none; }
|
||||
.calendar-day.other-month { color: #ccc; background: #f8f9fa; cursor: default; }
|
||||
.calendar-day.available { background: #fff; color: #333; }
|
||||
.calendar-day.available:hover { background: #e3f2fd; color: #1976d2; }
|
||||
.calendar-day.unavailable { background: #f5f5f5; color: #999; cursor: not-allowed; }
|
||||
.calendar-day.full { background: #fbe9e7; color: #c62828; cursor: not-allowed; } /* 예약 마감 */
|
||||
.calendar-day.holiday { background: #e8eaf6; color: #3f51b5; cursor: not-allowed; } /* 휴일 */
|
||||
.calendar-day.selected { background: #007bff; color: #fff; font-weight: 600; }
|
||||
.calendar-day.today { font-weight: 600; color: #dc3545; }
|
||||
.calendar-loading, .loading, .error, .no-slots { grid-column: 1 / -1; text-align: center; padding: 40px 20px; color: #666; font-style: italic; }
|
||||
.error { color: #dc3545; }
|
||||
|
||||
/* 범례 */
|
||||
.calendar-legend, .time-legend { display: flex; justify-content: center; gap: 20px; margin-top: 15px; flex-wrap: wrap; }
|
||||
.legend-item { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #666; }
|
||||
.legend-color { width: 12px; height: 12px; border-radius: 2px; border: 1px solid #ddd; }
|
||||
.legend-color.available { background: #fff; }
|
||||
.legend-color.unavailable { background: #f5f5f5; }
|
||||
.legend-color.full { background: #fbe9e7; }
|
||||
.legend-color.holiday { background: #e8eaf6; }
|
||||
.legend-color.selected { background: #007bff; }
|
||||
.legend-color.time-available { background: #e8f5e8; border-color: #28a745; }
|
||||
.legend-color.time-full { background: #f8d7da; border-color: #dc3545; }
|
||||
.legend-color.time-too-soon { background: #f1f3f5; border-color: #dee2e6; }
|
||||
.legend-color.time-selected { background: #007bff; border-color: #007bff; }
|
||||
|
||||
/* 시간 선택 */
|
||||
.selected-date-info { margin-bottom: 10px; }
|
||||
.step-description { margin-bottom: 20px; }
|
||||
.step-description h4 { margin: 0 0 10px 0; font-size: 18px; }
|
||||
.step-description p { margin: 0; font-size: 14px; color: #666; }
|
||||
.selected-date-info h4 { margin: 0; color: #333; font-size: 16px; }
|
||||
.time-slots-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 20px; }
|
||||
.time-slot {
|
||||
padding: 12px 8px; border: 2px solid #e9ecef; border-radius: 6px; text-align: center; cursor: pointer;
|
||||
transition: all 0.2s ease; font-size: 14px; font-weight: 500; display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.time-text { font-weight: 600; }
|
||||
.slot-info { font-size: 11px; opacity: 0.8; }
|
||||
/* 💡 [수정] 예약 가능 슬롯 스타일 변경 */
|
||||
.time-slot.available { background: #e7f3ff; border-color: #007bff; color: #004085; }
|
||||
.time-slot.available:hover { background: #d4edda; transform: translateY(-1px); }
|
||||
.time-slot.full { background: #f8d7da; border-color: #dc3545; color: #721c24; cursor: not-allowed; }
|
||||
.time-slot.too-soon { background: #f1f3f5; color: #868e96; cursor: not-allowed; border-color: #dee2e6; }
|
||||
.time-slot.selected { background: #007bff; border-color: #007bff; color: #fff; }
|
||||
|
||||
/* 고객 정보 폼 */
|
||||
.reservation-summary { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.reservation-summary h5 { margin: 0 0 10px 0; font-size: 16px; }
|
||||
.summary-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
|
||||
.summary-item { display: flex; justify-content: space-between; font-size: 14px; }
|
||||
.summary-item .label { font-weight: 500; color: #495057; }
|
||||
.summary-item span { color: #333; }
|
||||
.customer-info-form h4 { margin: 0 0 15px 0; color: #333; font-size: 16px; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #495057; font-size: 14px; }
|
||||
.required { color: #dc3545; font-weight: bold; }
|
||||
.form-group input, .form-group textarea, .form-group select {
|
||||
width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px;
|
||||
font-size: 14px; transition: border-color 0.2s ease; box-sizing: border-box;
|
||||
}
|
||||
.form-group textarea { resize: vertical; min-height: 80px; }
|
||||
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
|
||||
outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* 네비게이션 버튼 */
|
||||
.reservation-nav-buttons {
|
||||
display: flex; justify-content: space-between; gap: 10px; margin-top: 25px;
|
||||
padding-top: 20px; border-top: 1px solid #e9ecef;
|
||||
}
|
||||
.reservation-nav-buttons button {
|
||||
padding: 12px 24px; border: none; border-radius: 6px; font-size: 14px;
|
||||
font-weight: 600; cursor: pointer; transition: all 0.2s ease; min-width: 100px;
|
||||
}
|
||||
.btn-prev { background: #6c757d; color: #fff; }
|
||||
.btn-prev:hover { background: #5a6268; }
|
||||
.btn-next, .btn-submit { background: #007bff; color: #fff; }
|
||||
.btn-next:hover, .btn-submit:hover { background: #0056b3; }
|
||||
|
||||
/* 반응형 디자인 - 모바일 최적화 */
|
||||
@media (max-width: 768px) {
|
||||
.reservation-modal-content { margin: 10px; max-height: 95vh; border-radius: 8px; }
|
||||
.reservation-modal-header, .reservation-modal-body { padding: 15px; }
|
||||
.reservation-modal-header h2 { font-size: 18px; }
|
||||
.reservation-steps { margin-bottom: 20px; }
|
||||
.step { padding: 0 10px; }
|
||||
.step-number { width: 28px; height: 28px; font-size: 13px; }
|
||||
.step-text { font-size: 11px; }
|
||||
.calendar-day { font-size: 13px; }
|
||||
.time-slots-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }
|
||||
.form-row { grid-template-columns: 1fr; gap: 0; }
|
||||
.form-group input, .form-group textarea, .form-group select { font-size: 16px; /* iOS 줌 방지 */ }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.reservation-modal-overlay { padding: 0; align-items: flex-end; }
|
||||
.reservation-modal-content { margin: 0; width: 100%; max-height: 90vh; border-radius: 10px 10px 0 0; }
|
||||
.reservation-nav-buttons { flex-direction: column-reverse; gap: 10px; }
|
||||
.reservation-nav-buttons button { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 💡 [개선] JavaScript 로직 전면 수정 -->
|
||||
<script>
|
||||
const ReservationPopup = {
|
||||
elements: {},
|
||||
state: {
|
||||
currentStep: 1,
|
||||
selectedDate: null,
|
||||
selectedTime: null,
|
||||
currentYear: new Date().getFullYear(),
|
||||
currentMonth: new Date().getMonth() + 1,
|
||||
},
|
||||
config: {
|
||||
ajaxUrl: '<?php echo $ajax_url; ?>',
|
||||
maxAdvanceDays: <?php echo (int)$max_advance_days; ?>,
|
||||
},
|
||||
|
||||
init() {
|
||||
this.elements.popup = document.getElementById('reservation-popup');
|
||||
if (!this.elements.popup) return;
|
||||
|
||||
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
|
||||
this.elements.form = this.elements.popup.querySelector('#reservation-form');
|
||||
this.elements.calendar = {
|
||||
year: this.elements.popup.querySelector('#calendar-year'),
|
||||
month: this.elements.popup.querySelector('#calendar-month'),
|
||||
days: this.elements.popup.querySelector('#calendar-days'),
|
||||
prevBtn: this.elements.popup.querySelector('.prev-month'),
|
||||
nextBtn: this.elements.popup.querySelector('.next-month'),
|
||||
};
|
||||
this.elements.timeSlotsGrid = this.elements.popup.querySelector('#time-slots-grid');
|
||||
this.elements.nav = {
|
||||
prevBtn: this.elements.popup.querySelector('.btn-prev'),
|
||||
nextBtn: this.elements.popup.querySelector('.btn-next'),
|
||||
submitBtn: this.elements.popup.querySelector('.btn-submit'),
|
||||
};
|
||||
|
||||
this.addEventListeners();
|
||||
},
|
||||
|
||||
addEventListeners() {
|
||||
// 💡 [수정] 닫기 버튼을 더 안정적으로 찾아 이벤트를 추가하고, null 체크를 추가합니다.
|
||||
const closeBtn = this.elements.popup.querySelector('.reservation-modal-close');
|
||||
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
|
||||
|
||||
this.elements.popup.addEventListener('click', e => {
|
||||
if (e.target === this.elements.popup) this.close();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && this.elements.popup.classList.contains('active')) this.close();
|
||||
});
|
||||
|
||||
// 💡 [수정] 모든 이벤트 리스너에 null 체크를 추가하여 스크립트 오류를 방지합니다.
|
||||
if (this.elements.calendar.prevBtn) this.elements.calendar.prevBtn.addEventListener('click', () => this.changeMonth(-1));
|
||||
if (this.elements.calendar.nextBtn) this.elements.calendar.nextBtn.addEventListener('click', () => this.changeMonth(1));
|
||||
|
||||
if (this.elements.nav.prevBtn) this.elements.nav.prevBtn.addEventListener('click', () => this.goToStep(this.state.currentStep - 1));
|
||||
if (this.elements.nav.nextBtn) this.elements.nav.nextBtn.addEventListener('click', () => this.goToNextStep());
|
||||
if (this.elements.form) {
|
||||
this.elements.form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.submitForm();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
open() {
|
||||
this.state.currentYear = new Date().getFullYear();
|
||||
this.state.currentMonth = new Date().getMonth() + 1;
|
||||
this.goToStep(1);
|
||||
this.renderCalendar();
|
||||
this.elements.popup.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
close() {
|
||||
this.elements.popup.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
this.elements.form.reset();
|
||||
},
|
||||
|
||||
changeMonth(delta) {
|
||||
this.state.currentMonth += delta;
|
||||
if (this.state.currentMonth < 1) {
|
||||
this.state.currentMonth = 12;
|
||||
this.state.currentYear--;
|
||||
} else if (this.state.currentMonth > 12) {
|
||||
this.state.currentMonth = 1;
|
||||
this.state.currentYear++;
|
||||
}
|
||||
this.renderCalendar();
|
||||
},
|
||||
|
||||
async renderCalendar() {
|
||||
this.elements.calendar.year.textContent = this.state.currentYear;
|
||||
this.elements.calendar.month.textContent = this.state.currentMonth;
|
||||
this.elements.calendar.days.innerHTML = '<div class="loading">달력 정보를 불러오는 중...</div>';
|
||||
|
||||
const availability = await this.fetchMonthAvailability();
|
||||
if (!availability) {
|
||||
this.elements.calendar.days.innerHTML = '<div class="error">달력 정보를 불러올 수 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.calendar.days.innerHTML = '';
|
||||
const firstDay = new Date(this.state.currentYear, this.state.currentMonth - 1, 1);
|
||||
const daysInMonth = new Date(this.state.currentYear, this.state.currentMonth, 0).getDate();
|
||||
const startDayOfWeek = firstDay.getDay();
|
||||
|
||||
for (let i = 0; i < startDayOfWeek; i++) {
|
||||
this.elements.calendar.days.appendChild(this.createDayElement(0, true));
|
||||
}
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
this.elements.calendar.days.appendChild(this.createDayElement(day, false, availability[day]));
|
||||
}
|
||||
},
|
||||
|
||||
createDayElement(day, isOtherMonth, availability = null) {
|
||||
const dayElement = document.createElement('div');
|
||||
dayElement.className = 'calendar-day';
|
||||
if (isOtherMonth) {
|
||||
dayElement.classList.add('other-month');
|
||||
return dayElement;
|
||||
}
|
||||
|
||||
|
||||
dayElement.textContent = day;
|
||||
|
||||
const dateStr = `${this.state.currentYear}-${String(this.state.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const currentDate = new Date(dateStr);
|
||||
currentDate.setHours(0, 0, 0, 0);
|
||||
if (currentDate.getTime() === today.getTime()) {
|
||||
dayElement.classList.add('today');
|
||||
} else if (currentDate.getTime() < today.getTime()){
|
||||
dayElement.classList.add('unavailable');
|
||||
} else if (availability && availability.available) {
|
||||
dayElement.classList.add('available');
|
||||
dayElement.addEventListener('click', () => this.selectDate(dateStr, dayElement));
|
||||
} else {
|
||||
// 💡 [개선] 예약 불가 사유에 따라 다른 스타일을 적용합니다.
|
||||
if (availability && availability.reason === 'holiday') {
|
||||
dayElement.classList.add('holiday');
|
||||
} else if (availability && availability.reason === 'full') {
|
||||
dayElement.classList.add('full');
|
||||
} else {
|
||||
dayElement.classList.add('unavailable');
|
||||
}
|
||||
}
|
||||
return dayElement;
|
||||
},
|
||||
|
||||
selectDate(dateStr, element) {
|
||||
const prevSelected = this.elements.calendar.days.querySelector('.selected');
|
||||
if (prevSelected) prevSelected.classList.remove('selected');
|
||||
element.classList.add('selected');
|
||||
this.state.selectedDate = dateStr;
|
||||
},
|
||||
|
||||
async fetchMonthAvailability() {
|
||||
this.showLoading();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'get_month_availability');
|
||||
formData.append('year', this.state.currentYear);
|
||||
formData.append('month', this.state.currentMonth);
|
||||
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
return result.success ? result.data : null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching month availability:', error);
|
||||
return null;
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTimeSlots() {
|
||||
this.showLoading();
|
||||
this.elements.timeSlotsGrid.innerHTML = '';
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'get_time_slots');
|
||||
formData.append('date', this.state.selectedDate);
|
||||
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderTimeSlots(result.data);
|
||||
} else {
|
||||
this.elements.timeSlotsGrid.innerHTML = `<div class="error">${result.message || '시간 정보를 불러올 수 없습니다.'}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching time slots:', error);
|
||||
this.elements.timeSlotsGrid.innerHTML = '<div class="error">오류가 발생했습니다. 다시 시도해주세요.</div>';
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
renderTimeSlots(slots) {
|
||||
// 💡 [수정] 슬롯이 하나도 없는 경우와, 예약 가능한 슬롯이 없는 경우를 구분하여 메시지를 표시합니다.
|
||||
if (slots.length === 0) {
|
||||
this.elements.timeSlotsGrid.innerHTML = '<div class="no-slots">해당 날짜에 운영되는 상담 시간이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let hasAvailableSlots = false;
|
||||
this.elements.timeSlotsGrid.innerHTML = ''; // 그리드 초기화
|
||||
|
||||
slots.forEach(slot => {
|
||||
const slotElement = document.createElement('div');
|
||||
slotElement.className = 'time-slot';
|
||||
|
||||
let slotInfoText = '';
|
||||
const reservedCount = slot.reserved_count;
|
||||
|
||||
if (slot.available) {
|
||||
hasAvailableSlots = true;
|
||||
slotElement.classList.add('available');
|
||||
slotElement.addEventListener('click', () => this.selectTime(slot.time, slotElement));
|
||||
slotInfoText = `예약 ${reservedCount} / ${slot.max_persons}`;
|
||||
} else {
|
||||
if (slot.reason === 'full') {
|
||||
slotElement.classList.add('full');
|
||||
slotInfoText = '마감';
|
||||
} else if (slot.reason === 'too_soon') {
|
||||
slotElement.classList.add('too-soon');
|
||||
slotInfoText = '예약 임박';
|
||||
} else {
|
||||
slotElement.classList.add('full'); // Fallback
|
||||
slotInfoText = '마감';
|
||||
}
|
||||
}
|
||||
slotElement.innerHTML = `<span class="time-text">${slot.time}</span> <span class="slot-info">${slotInfoText}</span>`;
|
||||
this.elements.timeSlotsGrid.appendChild(slotElement);
|
||||
});
|
||||
|
||||
// 예약 가능한 슬롯이 하나도 없는 경우 안내 메시지 추가
|
||||
if (!hasAvailableSlots) {
|
||||
const noSlotsMessage = document.createElement('div');
|
||||
noSlotsMessage.className = 'no-slots';
|
||||
noSlotsMessage.textContent = '현재 예약 가능한 시간이 없습니다. 다른 날짜를 선택해주세요.';
|
||||
this.elements.timeSlotsGrid.prepend(noSlotsMessage);
|
||||
}
|
||||
},
|
||||
|
||||
selectTime(time, element) {
|
||||
const prevSelected = this.elements.timeSlotsGrid.querySelector('.selected');
|
||||
if (prevSelected) prevSelected.classList.remove('selected');
|
||||
element.classList.add('selected');
|
||||
this.state.selectedTime = time;
|
||||
},
|
||||
|
||||
goToStep(step) {
|
||||
this.state.currentStep = step;
|
||||
this.elements.popup.querySelectorAll('.step').forEach((el, i) => {
|
||||
el.classList.toggle('active', i + 1 === step);
|
||||
el.classList.toggle('completed', i + 1 < step);
|
||||
});
|
||||
this.elements.popup.querySelectorAll('.reservation-step-content').forEach(el => {
|
||||
el.style.display = parseInt(el.dataset.step) === step ? 'block' : 'none';
|
||||
});
|
||||
|
||||
this.elements.nav.prevBtn.style.display = step > 1 ? 'inline-block' : 'none';
|
||||
this.elements.nav.nextBtn.style.display = step < 3 ? 'inline-block' : 'none';
|
||||
this.elements.nav.submitBtn.style.display = step === 3 ? 'inline-block' : 'none';
|
||||
},
|
||||
|
||||
goToNextStep() {
|
||||
if (this.state.currentStep === 1 && !this.state.selectedDate) {
|
||||
alert('날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (this.state.currentStep === 2 && !this.state.selectedTime) {
|
||||
alert('시간을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.currentStep < 3) {
|
||||
this.goToStep(this.state.currentStep + 1);
|
||||
if (this.state.currentStep === 2) this.fetchTimeSlots();
|
||||
if (this.state.currentStep === 3) this.updateSummary();
|
||||
}
|
||||
},
|
||||
|
||||
updateSummary() {
|
||||
const date = new Date(this.state.selectedDate);
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
|
||||
this.elements.popup.querySelector('#summary-date').textContent = date.toLocaleDateString('ko-KR', options);
|
||||
this.elements.popup.querySelector('#summary-time').textContent = this.state.selectedTime;
|
||||
this.elements.popup.querySelector('#selected-date-display').textContent = date.toLocaleDateString('ko-KR', options);
|
||||
this.elements.form.querySelector('#selected-date').value = this.state.selectedDate;
|
||||
this.elements.form.querySelector('#selected-time').value = this.state.selectedTime;
|
||||
},
|
||||
|
||||
async submitForm() {
|
||||
if (!this.elements.form.checkValidity()) {
|
||||
alert('필수 입력 항목을 모두 채워주세요.');
|
||||
this.elements.form.reportValidity();
|
||||
return;
|
||||
}
|
||||
this.showLoading();
|
||||
this.elements.nav.submitBtn.disabled = true;
|
||||
this.elements.nav.submitBtn.textContent = '처리 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(this.elements.form);
|
||||
const response = await fetch('<?php echo $form_action_url; ?>', { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('예약 신청이 완료되었습니다. 입금 확인 후 예약이 확정됩니다.');
|
||||
this.close();
|
||||
// 필요시 페이지 새로고침 또는 다른 동작 수행
|
||||
// location.reload();
|
||||
} else {
|
||||
alert(result.message || '예약 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
this.elements.nav.submitBtn.disabled = false;
|
||||
this.elements.nav.submitBtn.textContent = '예약 신청';
|
||||
}
|
||||
},
|
||||
|
||||
showLoading() { if (this.elements.loading) this.elements.loading.style.display = 'flex'; },
|
||||
hideLoading() { if (this.elements.loading) this.elements.loading.style.display = 'none'; },
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => ReservationPopup.init());
|
||||
|
||||
// 외부에서 팝업을 열기 위한 전역 함수
|
||||
function openReservationPopup() {
|
||||
ReservationPopup.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php } // end if($consultant_installed) ?>
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// 입력 데이터 정리
|
||||
$reservation_data = [
|
||||
'customer_name' => trim($_POST['customer_name'] ?? ''),
|
||||
'customer_phone' => trim($_POST['customer_phone'] ?? ''),
|
||||
'customer_email' => trim($_POST['customer_email'] ?? ''),
|
||||
'reservation_date' => trim($_POST['reservation_date'] ?? ''),
|
||||
'reservation_time' => trim($_POST['reservation_time'] ?? ''),
|
||||
'consultation_type' => trim($_POST['consultation_type'] ?? 'onsite'),
|
||||
'request_memo' => trim($_POST['customer_request'] ?? ''),
|
||||
'payment_amount' => (int)($_POST['payment_amount'] ?? 0),
|
||||
'status' => 'payment_pending'
|
||||
];
|
||||
|
||||
// 필수 항목 유효성 검사
|
||||
if (empty($reservation_data['customer_name']) || empty($reservation_data['customer_phone']) || empty($reservation_data['reservation_date']) || empty($reservation_data['reservation_time'])) {
|
||||
throw new Exception('필수 예약 정보가 누락되었습니다.');
|
||||
}
|
||||
|
||||
// 예약 가능 여부 재확인 (서버 측 검증)
|
||||
$sql_check = "SELECT COUNT(*) as cnt FROM consultant_schedule
|
||||
WHERE specific_date = '{$reservation_data['reservation_date']}'
|
||||
AND start_time = '{$reservation_data['reservation_time']}'
|
||||
AND is_available = 1";
|
||||
$schedule = sql_fetch($sql_check);
|
||||
|
||||
if (!$schedule || $schedule['cnt'] == 0) {
|
||||
throw new Exception('선택하신 시간은 예약이 불가능합니다. 다른 시간을 선택해주세요.');
|
||||
}
|
||||
|
||||
// 예약 생성
|
||||
$sql = "INSERT INTO consultant_reservations
|
||||
(customer_name, customer_phone, customer_email, reservation_date, reservation_time, consultation_type, request_memo, payment_amount, status, created_at, updated_at)
|
||||
VALUES
|
||||
(
|
||||
'" . sql_real_escape_string($reservation_data['customer_name']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['customer_phone']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['customer_email']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['reservation_date']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['reservation_time']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['consultation_type']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['request_memo']) . "',
|
||||
'{$reservation_data['payment_amount']}',
|
||||
'{$reservation_data['status']}',
|
||||
NOW(),
|
||||
NOW()
|
||||
)";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
$reservation_id = sql_insert_id();
|
||||
consultant_log("새 예약 신청: ID {$reservation_id} (고객: {$reservation_data['customer_name']})");
|
||||
|
||||
// TODO: 고객 및 관리자에게 알림 발송 로직 추가
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '예약 신청이 완료되었습니다.']);
|
||||
} else {
|
||||
throw new Exception('데이터베이스 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("예약 신청 오류: " . $e->getMessage(), 'error');
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
$sub_menu = '850100'; // 대시보드 메뉴와 동일하게 설정
|
||||
include_once('./_common.php');
|
||||
|
||||
$g5['title'] = '상담 예약 팝업 샘플';
|
||||
include_once(G5_ADMIN_PATH.'/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
이 페이지는 사이트의 어떤 페이지에서든 상담 예약 기능을 쉽게 추가하는 방법을 보여주는 예제입니다.<br>
|
||||
아래 코드 한 줄만 포함하면, 버튼과 팝업 기능이 모두 활성화됩니다.
|
||||
</p>
|
||||
<pre><code><?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?></code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [시작] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<div style="text-align:center; padding: 50px 20px; background-color:#f5f5f5; border-radius:10px; margin: 20px 0;">
|
||||
<h3 style="margin-bottom:15px;">상담이 필요하신가요?</h3>
|
||||
<p style="margin-bottom:25px; color:#666;">버튼을 눌러 간편하게 상담을 신청하거나, 기존 예약을 확인/취소할 수 있습니다.</p>
|
||||
|
||||
<!-- 1. 팝업을 여는 버튼들 -->
|
||||
<div class="consultant-buttons">
|
||||
<button type="button" class="reservation-btn-main" onclick="openReservationPopup()">
|
||||
<i class="fa fa-calendar"></i> 상담 예약 신청
|
||||
</button>
|
||||
<button type="button" class="reservation-check-btn" onclick="openReservationCheckPopup()">
|
||||
예약 확인/취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 2. 팝업 파일들 포함 (이 코드 한 줄이면 모든 팝업 기능이 로드됩니다)
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php');
|
||||
?>
|
||||
|
||||
<!-- 버튼 디자인을 위한 CSS (사이트의 공통 CSS 파일에 추가하는 것을 권장합니다) -->
|
||||
<style>
|
||||
.consultant-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.reservation-btn-main, .reservation-check-btn {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.reservation-btn-main {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
.reservation-btn-main:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
.reservation-btn-main i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.reservation-check-btn {
|
||||
background: #6c757d;
|
||||
}
|
||||
.reservation-check-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [끝] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH.'/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 대시보드
|
||||
*/
|
||||
$sub_menu = '850100';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 예약 대시보드';
|
||||
|
||||
// 오늘 날짜 기준 통계
|
||||
$today = date('Y-m-d');
|
||||
$this_week_start = date('Y-m-d', strtotime('monday this week'));
|
||||
$this_month_start = date('Y-m-01');
|
||||
|
||||
// 오늘 예약 현황
|
||||
$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
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date = '{$today}' AND is_deleted = 0";
|
||||
$today_stats = sql_fetch($sql);
|
||||
|
||||
// 이번 주 예약 현황
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$this_week_start}'
|
||||
AND reservation_date <= '{$today}'
|
||||
AND is_deleted = 0";
|
||||
$week_stats = sql_fetch($sql);
|
||||
|
||||
// 이번 달 예약 현황
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$this_month_start}'
|
||||
AND is_deleted = 0";
|
||||
$month_stats = sql_fetch($sql);
|
||||
|
||||
// 최근 예약 목록 (5개)
|
||||
$sql = "SELECT * FROM consultant_reservations
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5";
|
||||
$recent_reservations = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$recent_reservations[] = $row;
|
||||
}
|
||||
|
||||
// 오늘 예약 목록
|
||||
$sql = "SELECT * FROM consultant_reservations
|
||||
WHERE reservation_date = '{$today}'
|
||||
AND is_deleted = 0
|
||||
ORDER BY reservation_time";
|
||||
$today_reservations = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$today_reservations[] = $row;
|
||||
}
|
||||
|
||||
// 입금 대기 예약 수
|
||||
$sql = "SELECT COUNT(*) as count FROM consultant_reservations
|
||||
WHERE status = 'payment_pending' AND is_deleted = 0";
|
||||
$pending_count = sql_fetch($sql)['count'];
|
||||
|
||||
// 시간대별 예약 현황 (이번 주)
|
||||
$sql = "SELECT
|
||||
reservation_time,
|
||||
COUNT(*) as count
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$this_week_start}'
|
||||
AND reservation_date <= '{$today}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_time
|
||||
ORDER BY reservation_time";
|
||||
$time_stats = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$time_stats[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.dashboard-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-sublabel {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-card.today .stat-number {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.stat-card.week .stat-number {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.stat-card.month .stat-number {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.stat-card.pending .stat-number {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.reservation-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.reservation-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.reservation-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reservation-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.reservation-details {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.reservation-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-around;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
background: #007bff;
|
||||
width: 30px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.chart-bar:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-value {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 10px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #856404;
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<div>
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
<p>상담 예약 현황을 한눈에 확인하세요</p>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<a href="reservations.php" class="btn btn-primary">예약 관리</a>
|
||||
<a href="schedule_generate.php" class="btn btn-success">일정 설정</a>
|
||||
<a href="statistics.php" class="btn btn-info">통계 보기</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card today">
|
||||
<div class="stat-number"><?php echo number_format($today_stats['total']); ?></div>
|
||||
<div class="stat-label">오늘 예약</div>
|
||||
<div class="stat-sublabel">
|
||||
확정 <?php echo $today_stats['confirmed']; ?>건 |
|
||||
대기 <?php echo $today_stats['pending']; ?>건
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card week">
|
||||
<div class="stat-number"><?php echo number_format($week_stats['total']); ?></div>
|
||||
<div class="stat-label">이번 주 예약</div>
|
||||
<div class="stat-sublabel">
|
||||
매출 <?php echo number_format($week_stats['revenue']); ?>원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card month">
|
||||
<div class="stat-number"><?php echo number_format($month_stats['total']); ?></div>
|
||||
<div class="stat-label">이번 달 예약</div>
|
||||
<div class="stat-sublabel">
|
||||
매출 <?php echo number_format($month_stats['revenue']); ?>원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card pending">
|
||||
<div class="stat-number"><?php echo number_format($pending_count); ?></div>
|
||||
<div class="stat-label">입금 대기</div>
|
||||
<div class="stat-sublabel">
|
||||
<?php if ($pending_count > 0): ?>
|
||||
<a href="reservations.php?status=payment_pending" style="color: #856404;">확인 필요</a>
|
||||
<?php else: ?>
|
||||
모두 처리됨
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($pending_count > 0): ?>
|
||||
<div class="alert alert-warning">
|
||||
<strong>알림:</strong> 입금 대기 중인 예약이 <?php echo $pending_count; ?>건 있습니다.
|
||||
<a href="reservations.php?status=payment_pending">지금 확인하기</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<div class="content-grid">
|
||||
<!-- 오늘 예약 현황 -->
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<span>오늘 예약 현황 (<?php echo date('Y-m-d'); ?>)</span>
|
||||
<a href="reservations.php?date=<?php echo $today; ?>" class="btn btn-sm btn-primary">전체 보기</a>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<?php if (empty($today_reservations)): ?>
|
||||
<div class="empty-state">
|
||||
<p>오늘 예약된 상담이 없습니다.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($today_reservations as $reservation): ?>
|
||||
<div class="reservation-item">
|
||||
<div class="reservation-info">
|
||||
<div class="reservation-name">
|
||||
<?php echo htmlspecialchars($reservation['customer_name']); ?>
|
||||
</div>
|
||||
<div class="reservation-details">
|
||||
<?php echo $reservation['reservation_time']; ?> |
|
||||
<?php echo htmlspecialchars($reservation['customer_phone']); ?> |
|
||||
<?php echo number_format($reservation['payment_amount']); ?>원
|
||||
</div>
|
||||
</div>
|
||||
<div class="reservation-status status-<?php echo $reservation['status']; ?>">
|
||||
<?php
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
echo $status_labels[$reservation['status']] ?? $reservation['status'];
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 예약 -->
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<span>최근 예약</span>
|
||||
<a href="reservations.php" class="btn btn-sm btn-primary">전체 보기</a>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<?php if (empty($recent_reservations)): ?>
|
||||
<div class="empty-state">
|
||||
<p>최근 예약이 없습니다.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recent_reservations as $reservation): ?>
|
||||
<div class="reservation-item">
|
||||
<div class="reservation-info">
|
||||
<div class="reservation-name">
|
||||
<?php echo htmlspecialchars($reservation['customer_name']); ?>
|
||||
</div>
|
||||
<div class="reservation-details">
|
||||
<?php echo $reservation['reservation_date']; ?>
|
||||
<?php echo $reservation['reservation_time']; ?><br>
|
||||
<?php echo date('m-d H:i', strtotime($reservation['created_at'])); ?> 신청
|
||||
</div>
|
||||
</div>
|
||||
<div class="reservation-status status-<?php echo $reservation['status']; ?>">
|
||||
<?php
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
echo $status_labels[$reservation['status']] ?? $reservation['status'];
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 시간대별 예약 현황 -->
|
||||
<?php if (!empty($time_stats)): ?>
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<span>시간대별 예약 현황 (이번 주)</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="chart-container">
|
||||
<?php
|
||||
$max_count = max(array_column($time_stats, 'count'));
|
||||
foreach ($time_stats as $stat):
|
||||
$height = $max_count > 0 ? ($stat['count'] / $max_count) * 150 : 0;
|
||||
?>
|
||||
<div class="chart-bar" style="height: <?php echo $height; ?>px;">
|
||||
<div class="chart-value"><?php echo $stat['count']; ?></div>
|
||||
<div class="chart-label"><?php echo substr($stat['reservation_time'], 0, 5); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 시스템 정보 -->
|
||||
<div class="alert alert-info">
|
||||
<strong>시스템 정보:</strong>
|
||||
상담 예약 시스템 v<?php echo G5_CONSULTANT_VERSION; ?> |
|
||||
마지막 업데이트: <?php echo date('Y-m-d H:i'); ?> |
|
||||
<a href="settings.php">시스템 설정</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 실시간 업데이트 (5분마다)
|
||||
setInterval(function () {
|
||||
location.reload();
|
||||
}, 300000);
|
||||
|
||||
// 차트 애니메이션
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const bars = document.querySelectorAll('.chart-bar');
|
||||
bars.forEach((bar, index) => {
|
||||
setTimeout(() => {
|
||||
bar.style.opacity = '0';
|
||||
bar.style.transform = 'scaleY(0)';
|
||||
bar.style.transformOrigin = 'bottom';
|
||||
|
||||
setTimeout(() => {
|
||||
bar.style.transition = 'all 0.5s ease';
|
||||
bar.style.opacity = '1';
|
||||
bar.style.transform = 'scaleY(1)';
|
||||
}, 100);
|
||||
}, index * 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 상담 관리 시스템 버전
|
||||
define('G5_CONSULTANT_VERSION', '1.0.0');
|
||||
|
||||
// 상담 관리 시스템 설치 확인
|
||||
function is_consultant_installed()
|
||||
{
|
||||
global $g5;
|
||||
$consultant_tables = ['consultant_schedule', 'consultant_reservations', 'consultant_config', 'consultant_log'];
|
||||
|
||||
foreach ($consultant_tables as $table) {
|
||||
if(!isset($g5[$table.'_table'])) $g5[$table.'_table'] = $table;
|
||||
$sql = "SHOW TABLES LIKE '{$g5[$table.'_table']}'";
|
||||
$result = sql_query($sql, false);
|
||||
if (!$result || sql_num_rows($result) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 상담 관리 시스템 권한 확인
|
||||
function consultant_auth_check($auth_level = 'r')
|
||||
{
|
||||
global $member, $is_admin;
|
||||
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 추가 권한 확인 로직 (필요시)
|
||||
return true;
|
||||
}
|
||||
|
||||
// 상담 관리 시스템 설정값 조회
|
||||
function consultant_get_config($key, $default = null)
|
||||
{
|
||||
global $g5;
|
||||
if(!isset($g5['consultant_config_table'])) $g5['consultant_config_table'] = 'consultant_config';
|
||||
|
||||
$sql = "SELECT config_value FROM {$g5['consultant_config_table']} WHERE config_key = '" . sql_real_escape_string($key) . "'";
|
||||
$result = sql_fetch($sql);
|
||||
|
||||
if ($result) {
|
||||
return $result['config_value'];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 상담 관리 시스템 설정값 저장
|
||||
function consultant_set_config($key, $value)
|
||||
{
|
||||
global $g5;
|
||||
if(!isset($g5['consultant_config_table'])) $g5['consultant_config_table'] = 'consultant_config';
|
||||
|
||||
$sql = "INSERT INTO {$g5['consultant_config_table']} (config_key, config_value, updated_at)
|
||||
VALUES ('" . sql_real_escape_string($key) . "', '" . sql_real_escape_string($value) . "', NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_value = '" . sql_real_escape_string($value) . "',
|
||||
updated_at = NOW()";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
// 로그 함수
|
||||
function consultant_log($message, $level = 'info')
|
||||
{
|
||||
global $g5, $member;
|
||||
if(!isset($g5['consultant_log_table'])) $g5['consultant_log_table'] = 'consultant_log';
|
||||
|
||||
$sql = "INSERT INTO {$g5['consultant_log_table']} (mb_id, log_level, log_message, ip_address, log_time) VALUES ('".($member['mb_id'] ?? '')."', '".sql_real_escape_string($level)."', '".sql_real_escape_string($message)."', '".$_SERVER['REMOTE_ADDR']."', '".G5_TIME_YMDHIS."')";
|
||||
sql_query($sql, false);
|
||||
}
|
||||
|
||||
// 💡 [추가] 알림 발송 함수 (메일/SMS 통합)
|
||||
function consultant_send_notification($type, $template_key, $data) {
|
||||
global $g5;
|
||||
|
||||
// 템플릿 조회
|
||||
$table_name = ($type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
|
||||
|
||||
// 테이블 존재 확인
|
||||
$table_check = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
|
||||
if (sql_num_rows($table_check) == 0) return false;
|
||||
|
||||
$sql = "SELECT * FROM {$table_name} WHERE template_key = '" . sql_real_escape_string($template_key) . "'";
|
||||
$template = sql_fetch($sql);
|
||||
|
||||
if (!$template) return false;
|
||||
|
||||
// 변수 치환
|
||||
$subject = $template['template_subject'];
|
||||
$content = $template['template_content'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$subject = str_replace('{' . $key . '}', $value, $subject);
|
||||
$content = str_replace('{' . $key . '}', $value, $content);
|
||||
}
|
||||
|
||||
if ($type === 'email') {
|
||||
// 이메일 발송 (G5 기본 mailer 사용 가정)
|
||||
include_once(G5_LIB_PATH.'/mailer.lib.php');
|
||||
// $data['customer_email'] 등이 존재해야 함
|
||||
if (!empty($data['customer_email'])) {
|
||||
mailer($g5['title'], $g5['admin_email'], $data['customer_email'], $subject, $content, 1);
|
||||
consultant_log("이메일 발송: {$template_key} to {$data['customer_email']}");
|
||||
return true;
|
||||
}
|
||||
} elseif ($type === 'sms') {
|
||||
// SMS 발송 (G5 기본 SMS 라이브러리 사용 가정 - 실제 구현은 SMS 모듈에 따라 다름)
|
||||
// 여기서는 로그만 남기고 실제 발송 로직은 SMS 모듈에 맞게 구현 필요
|
||||
// 예:
|
||||
// include_once(G5_LIB_PATH.'/icode.sms.lib.php');
|
||||
// $SMS = new SMS;
|
||||
// $SMS->SMS_con($g5['sms_admin'], $g5['sms_id'], $g5['sms_pw'], $g5['sms_port']);
|
||||
// $SMS->Add($data['customer_phone'], $g5['admin_phone'], $g5['sms_admin'], iconv("utf-8", "euc-kr", $content), "");
|
||||
// $SMS->Send();
|
||||
|
||||
consultant_log("SMS 발송 시도: {$template_key} to {$data['customer_phone']} (내용: $content)");
|
||||
return true; // 실제 발송 성공 여부에 따라 변경
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
$sub_menu = '850640';
|
||||
include_once('./_common.php');
|
||||
include_once(__DIR__ . '/lib/SchemaManager.class.php');
|
||||
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 파일에서 테이블 이름을 추출하는 함수
|
||||
*/
|
||||
function get_tables_from_sql_file($filepath) {
|
||||
$tables = [];
|
||||
if (!file_exists($filepath)) return $tables;
|
||||
$lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/CREATE TABLE(?: IF NOT EXISTS)? `([^`]+)`/i', $line, $matches)) {
|
||||
$tables[] = $matches[1];
|
||||
}
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 예약 시스템 설치';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
$install_result = null;
|
||||
$delete_result = null;
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
$tables_to_check = get_tables_from_sql_file(__DIR__ . '/install.sql');
|
||||
|
||||
if ($action === 'install') {
|
||||
check_admin_token();
|
||||
try {
|
||||
$sql_file = __DIR__ . '/install.sql';
|
||||
$schemaManager = new SchemaManager($sql_file);
|
||||
$schemaManager->execute();
|
||||
$db_results = $schemaManager->get_results();
|
||||
$data_msg = insert_default_data() ? "성공" : "실패";
|
||||
$menu_msg = create_admin_menu_file();
|
||||
$install_result = ['db' => $db_results, 'data' => $data_msg, 'menu' => $menu_msg];
|
||||
} catch (Exception $e) {
|
||||
$install_result['errors'][] = '설치 중 심각한 오류 발생: ' . $e->getMessage();
|
||||
}
|
||||
} else if ($action === 'delete') {
|
||||
check_admin_token();
|
||||
$delete_result = ['tables' => [], 'menu' => ''];
|
||||
$tables_to_delete = $tables_to_check;
|
||||
foreach ($tables_to_delete as $table) {
|
||||
sql_query("DROP TABLE IF EXISTS `{$table}`", false);
|
||||
$delete_result['tables'][] = $table;
|
||||
}
|
||||
$menu_file = G5_ADMIN_PATH . '/admin.menu850.consultant_manage.php';
|
||||
if (file_exists($menu_file)) {
|
||||
if (@unlink($menu_file)) {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 성공';
|
||||
} else {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insert_default_data() {
|
||||
$default_configs = [
|
||||
['consultation_duration', '60', '1회 상담 시간 (분)'], ['max_persons_per_slot', '2', '동시간대 최대 예약 인원'],
|
||||
['consultation_fee', '50000', '상담 비용 (원)'], ['account_info', '국민은행 123-456-789 (주)상담센터', '상담비 입금 계좌'],
|
||||
['notification_enabled', '1', '알림 발송 사용 여부'], ['auto_confirm_enabled', '0', '자동 예약 확정 사용 여부'],
|
||||
['max_advance_days', '30', '최대 예약 가능 일수'], ['min_advance_hours', '24', '최소 예약 시간 (시간)'],
|
||||
['cancel_deadline_hours', '24', '예약 취소 마감 시간 (시간)']
|
||||
];
|
||||
foreach ($default_configs as $config) {
|
||||
sql_query("INSERT IGNORE INTO consultant_config (config_key, config_value, config_desc) VALUES ('{$config[0]}', '{$config[1]}', '{$config[2]}')");
|
||||
}
|
||||
|
||||
// 💡 [수정] 메일 템플릿 기본 데이터
|
||||
$mail_templates = [
|
||||
['consultant_reservation_customer', '고객 예약 신청 확인', '[상담예약] 예약 신청이 접수되었습니다', "안녕하세요 {customer_name}님,\n\n상담 예약 신청이 정상적으로 접수되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n- 상담비: {payment_amount}원\n\n입금 계좌: {account_info}\n\n입금 확인 후 예약이 확정됩니다.\n\n감사합니다."],
|
||||
['consultant_confirmed_customer', '고객 예약 확정 알림', '[상담예약] 예약이 확정되었습니다', "안녕하세요 {customer_name}님,\n\n입금이 확인되어 예약이 확정되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n상담 당일 시간에 맞춰 방문해주시기 바랍니다.\n\n감사합니다."],
|
||||
['consultant_cancelled_customer', '고객 예약 취소 알림', '[상담예약] 예약이 취소되었습니다', "안녕하세요 {customer_name}님,\n\n예약이 취소되었습니다.\n\n취소된 예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n취소 사유: {cancel_reason}\n\n문의사항이 있으시면 연락주시기 바랍니다.\n\n감사합니다."]
|
||||
];
|
||||
foreach ($mail_templates as $template) {
|
||||
sql_query("INSERT IGNORE INTO consultant_mail_templates (template_key, template_name, template_subject, template_content) VALUES ('{$template[0]}', '{$template[1]}', '{$template[2]}', '" . sql_real_escape_string($template[3]) . "')");
|
||||
}
|
||||
|
||||
// 💡 [추가] SMS 템플릿 기본 데이터
|
||||
$sms_templates = [
|
||||
['consultant_reservation_customer', '고객 예약 신청 확인', '[상담예약] 예약 신청 접수', "{customer_name}님, 상담 예약이 접수되었습니다.\n일시: {reservation_date} {reservation_time}\n계좌: {account_info}\n입금 확인 후 확정됩니다."],
|
||||
['consultant_confirmed_customer', '고객 예약 확정 알림', '[상담예약] 예약 확정', "{customer_name}님, 예약이 확정되었습니다.\n일시: {reservation_date} {reservation_time}\n시간 맞춰 방문 부탁드립니다."],
|
||||
['consultant_cancelled_customer', '고객 예약 취소 알림', '[상담예약] 예약 취소', "{customer_name}님, 예약이 취소되었습니다.\n일시: {reservation_date} {reservation_time}\n사유: {cancel_reason}"]
|
||||
];
|
||||
foreach ($sms_templates as $template) {
|
||||
sql_query("INSERT IGNORE INTO consultant_sms_templates (template_key, template_name, template_subject, template_content) VALUES ('{$template[0]}', '{$template[1]}', '{$template[2]}', '" . sql_real_escape_string($template[3]) . "')");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function create_admin_menu_file() {
|
||||
$source_file = __DIR__ . '/admin.menu850.consultant_manage.php';
|
||||
$target_file = G5_ADMIN_PATH . '/admin.menu850.consultant_manage.php';
|
||||
if (!file_exists($source_file)) return "실패 (메뉴 원본 파일 없음)";
|
||||
if (file_exists($target_file)) return "성공 (이미 존재함)";
|
||||
if (@copy($source_file, $target_file)) return "성공";
|
||||
return "실패 (파일 복사 오류)";
|
||||
}
|
||||
|
||||
$existing_tables = [];
|
||||
foreach ($tables_to_check as $table) {
|
||||
if (sql_query("SHOW TABLES LIKE '$table'", false) && sql_num_rows(sql_query("SHOW TABLES LIKE '$table'", false)) > 0) {
|
||||
$existing_tables[] = $table;
|
||||
}
|
||||
}
|
||||
$is_installed = count($existing_tables) == count($tables_to_check);
|
||||
?>
|
||||
|
||||
<style>
|
||||
.install-container { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.install-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #AA20FF; }
|
||||
.install-header h1 { color: #AA20FF; margin-bottom: 10px; }
|
||||
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0; }
|
||||
.feature-card { padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center; }
|
||||
.feature-card i { font-size: 2em; color: #AA20FF; margin-bottom: 10px; }
|
||||
.status-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
.status-table th, .status-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
.status-table th { background-color: #fff; font-weight: bold; }
|
||||
.status-ok { color: #28a745; font-weight: bold; }
|
||||
.status-missing { color: #dc3545; font-weight: bold; }
|
||||
.install-btn { display: block; width: 200px; margin: 30px auto; padding: 15px 30px; background: #AA20FF; color: white; text-align: center; text-decoration: none; border-radius: 5px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; transition: background-color 0.3s; }
|
||||
.install-btn:hover { background: #8A1ACC; color: white; }
|
||||
.install-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||
.alert { padding: 15px; margin: 20px 0; border-radius: 5px; }
|
||||
.alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
|
||||
.alert-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
|
||||
.alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
|
||||
.btn-secondary { background: #6c757d; color: white; border-color: #6c757d; padding: 5px 10px; border-radius: 4px; text-decoration: none; }
|
||||
.btn-secondary:hover { background: #5a6268; }
|
||||
.btn-danger { background: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
|
||||
.btn-danger:hover { background: #c82333; }
|
||||
.button-group { display: flex; justify-content: center; align-items: center; gap: 10px; }
|
||||
</style>
|
||||
|
||||
<div class="install-container">
|
||||
<div class="install-header">
|
||||
<h1><i class="fa fa-calendar-check"></i> 상담 예약 시스템</h1>
|
||||
<p>전문적인 상담 예약 관리 및 일정 관리 솔루션</p>
|
||||
</div>
|
||||
<?php if ($install_result): ?>
|
||||
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 작업 완료</h4>
|
||||
<p>데이터베이스 및 기본 설정 설치가 완료되었습니다.</p>
|
||||
<p><?php echo 'data : '.$install_result['data']; ?></p>
|
||||
<p><?php echo 'menu : '.$install_result['menu']; ?></p>
|
||||
<p><a href="./dashboard.php" class="btn btn-primary">상담 예약 관리로 이동</a></p></div>
|
||||
<?php elseif ($delete_result): ?>
|
||||
<div class="alert alert-danger"><h4><i class="fa fa-trash"></i> 삭제 작업 완료</h4><p>솔루션 관련 데이터와 파일이 삭제되었습니다.</p><ul><?php foreach($delete_result['tables'] as $tbl) echo "<li>{$tbl} 테이블 삭제됨</li>"; ?><li><?php echo $delete_result['menu']; ?></li></ul></div>
|
||||
<?php elseif ($is_installed): ?>
|
||||
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 완료</h4><p>상담 예약 시스템이 이미 설치되어 있습니다.</p><p><a href="./dashboard.php" class="btn btn-primary">상담 예약 관리로 이동</a></p></div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info"><h4><i class="fa fa-info-circle"></i> 설치 필요</h4><p>상담 예약 시스템을 사용하기 위해 설치가 필요합니다.</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3><i class="fa fa-database"></i> 설치 상태</h3>
|
||||
<table class="status-table">
|
||||
<thead><tr><th>테이블명</th><th>설명</th><th>상태</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($tables_to_check as $table): ?>
|
||||
<tr>
|
||||
<td><code><?php echo $table; ?></code></td>
|
||||
<td><?php echo array('consultant_config' => '환경 설정', 'consultant_schedule' => '상담 스케줄', 'consultant_reservations' => '예약 정보', 'consultant_mail_templates' => '메일 템플릿', 'consultant_sms_templates' => '문자 템플릿', 'consultant_log' => '시스템 로그')[$table] ?? '데이터 테이블'; ?></td>
|
||||
<td>
|
||||
<?php if (in_array($table, $existing_tables)): ?>
|
||||
<span class="status-ok"><i class="fa fa-check"></i> 설치됨</span>
|
||||
<?php else: ?>
|
||||
<span class="status-missing"><i class="fa fa-times"></i> 미설치</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if (!$is_installed): ?>
|
||||
<form method="post" onsubmit="return confirm('솔루션을 설치하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="install-btn"><i class="fa fa-download"></i> 솔루션 설치하기</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_installed && !$install_result && !$delete_result): ?>
|
||||
<div class="button-group" style="text-align: center; margin-top: 20px;">
|
||||
<form method="post" onsubmit="return confirm('기존 데이터는 유지되며, 변경된 DB 구조만 업데이트 됩니다. 진행하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn btn-secondary"><i class="fa fa-sync"></i> 재설치 (업데이트)</button>
|
||||
</form>
|
||||
<form method="post" onsubmit="return confirm('정말로 솔루션을 삭제하시겠습니까? 모든 관련 데이터와 파일이 영구적으로 삭제됩니다.');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn-danger"><i class="fa fa-trash"></i> 솔루션 삭제하기</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,159 @@
|
||||
-- 1. 상담 설정 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_config` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`config_key` varchar(100) NOT NULL COMMENT '설정 키',
|
||||
`config_value` text COMMENT '설정 값',
|
||||
`config_desc` varchar(255) DEFAULT NULL COMMENT '설정 설명',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `config_key` (`config_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 시스템 설정';
|
||||
|
||||
-- 2. 상담 일정 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_schedule` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`specific_date` date DEFAULT NULL COMMENT '특정 날짜',
|
||||
`start_time` time NOT NULL COMMENT '시작 시간',
|
||||
`end_time` time NOT NULL COMMENT '종료 시간',
|
||||
`time_slot` int(11) DEFAULT 60 COMMENT '예약 단위 시간(분)',
|
||||
`max_persons` int(11) DEFAULT 1 COMMENT '동시간대 최대 예약 인원',
|
||||
`is_available` tinyint(1) DEFAULT 1 COMMENT '예약 가능 여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_specific_date` (`specific_date`),
|
||||
KEY `idx_is_available` (`is_available`),
|
||||
KEY `idx_time_range` (`start_time`,`end_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 상세 스케줄';
|
||||
|
||||
-- 3. 상담 예약 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_reservations` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`wr_id` int(11) DEFAULT NULL COMMENT '연결된 게시글 ID',
|
||||
`customer_name` varchar(100) NOT NULL COMMENT '고객명',
|
||||
`customer_phone` varchar(20) NOT NULL COMMENT '고객 연락처',
|
||||
`customer_email` varchar(100) DEFAULT NULL COMMENT '고객 이메일',
|
||||
`reservation_date` date NOT NULL COMMENT '예약 날짜',
|
||||
`reservation_time` time NOT NULL COMMENT '예약 시간',
|
||||
`consultation_type` varchar(50) DEFAULT 'onsite' COMMENT '상담 유형',
|
||||
`resource_id` int(11) DEFAULT NULL COMMENT '배정된 리소스 ID',
|
||||
`status` varchar(50) DEFAULT 'payment_pending' COMMENT '예약 상태',
|
||||
`payment_amount` int(11) DEFAULT 0 COMMENT '상담 비용',
|
||||
`payment_status` varchar(50) DEFAULT 'pending' COMMENT '결제 상태',
|
||||
`payment_confirmed_at` datetime DEFAULT NULL COMMENT '입금 확인 시간',
|
||||
`payment_confirmed_by` varchar(50) DEFAULT NULL COMMENT '입금 확인자',
|
||||
`request_memo` text COMMENT '고객 요청사항',
|
||||
`admin_memo` text COMMENT '관리자 메모',
|
||||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '삭제 여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_reservation_date` (`reservation_date`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_customer_phone` (`customer_phone`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 정보';
|
||||
|
||||
-- 4. 상담 메일 알림 템플릿 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_mail_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_type` varchar(10) NOT NULL DEFAULT 'email' COMMENT '템플릿 종류 (email)',
|
||||
`template_name` varchar(200) NOT NULL,
|
||||
`template_subject` varchar(255) NOT NULL,
|
||||
`template_content` text NOT NULL,
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `template_key` (`template_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 메일 알림 템플릿';
|
||||
|
||||
-- 5. 상담 문자 알림 템플릿 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_sms_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_type` varchar(10) NOT NULL DEFAULT 'sms' COMMENT '템플릿 종류 (sms)',
|
||||
`template_name` varchar(200) NOT NULL,
|
||||
`template_subject` varchar(255) NOT NULL,
|
||||
`template_content` text NOT NULL,
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `template_key` (`template_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 문자 알림 템플릿';
|
||||
|
||||
-- 6. 시스템 로그 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_log` (
|
||||
`log_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`log_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`mb_id` varchar(20) DEFAULT NULL,
|
||||
`log_level` varchar(20) NOT NULL DEFAULT 'info',
|
||||
`log_message` text NOT NULL,
|
||||
`ip_address` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`log_id`),
|
||||
KEY `idx_log_time` (`log_time`),
|
||||
KEY `idx_mb_id` (`mb_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 시스템 로그';
|
||||
|
||||
-- 7. 상담 그룹 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_groups` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL COMMENT '그룹명',
|
||||
`is_active` tinyint(1) DEFAULT 1 COMMENT '사용여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담/자원 그룹';
|
||||
|
||||
-- 8. 상담 리소스 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_resources` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`group_id` int(11) NOT NULL COMMENT '그룹 ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '리소스명(이름/호실)',
|
||||
`description` varchar(255) DEFAULT NULL COMMENT '설명',
|
||||
`is_active` tinyint(1) DEFAULT 1 COMMENT '사용여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `group_id` (`group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='상담사/자원 목록';
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
class NotificationManager
|
||||
{
|
||||
/**
|
||||
* 템플릿 키를 기반으로 알림을 발송합니다.
|
||||
* @param string $template_key 템플릿 키 (e.g., 'reservation_confirmed')
|
||||
* @param array $data 치환될 데이터 배열 (e.g., ['customer_name' => '홍길동'])
|
||||
* @return bool 성공 여부
|
||||
*/
|
||||
public function sendNotification($template_key, $data)
|
||||
{
|
||||
$success = true;
|
||||
|
||||
// 1. 이메일 템플릿 조회 및 발송
|
||||
$email_sql = "SELECT * FROM consultant_mail_templates WHERE template_key = '" . sql_real_escape_string($template_key) . "' AND is_active = 1";
|
||||
$email_template = sql_fetch($email_sql);
|
||||
|
||||
if ($email_template) {
|
||||
$subject = $this->replaceVariables($email_template['template_subject'], $data);
|
||||
$content = $this->replaceVariables($email_template['template_content'], $data);
|
||||
|
||||
if (!$this->sendEmail($data['customer_email'], $data['customer_name'], $subject, $content)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. SMS 템플릿 조회 및 발송
|
||||
$sms_sql = "SELECT * FROM consultant_sms_templates WHERE template_key = '" . sql_real_escape_string($template_key) . "' AND is_active = 1";
|
||||
$sms_template = sql_fetch($sms_sql);
|
||||
|
||||
if ($sms_template) {
|
||||
$content = $this->replaceVariables($sms_template['template_content'], $data);
|
||||
|
||||
if (!$this->sendSms($data['customer_phone'], $content)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 변수를 실제 값으로 치환합니다.
|
||||
*/
|
||||
private function replaceVariables($text, $data)
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$text = str_replace('{' . $key . '}', $value, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 💡 [연동] mail_manage 시스템을 사용하여 메일을 발송합니다.
|
||||
*/
|
||||
private function sendEmail($to_email, $to_name, $subject, $content)
|
||||
{
|
||||
// mail_manage의 라이브러리 포함
|
||||
if (file_exists(G5_ADMIN_PATH . '/mail_manage/mailer.lib.php')) {
|
||||
include_once(G5_ADMIN_PATH . '/mail_manage/mailer.lib.php');
|
||||
|
||||
// mailer() 함수 호출 (mail_manage의 함수명에 따라 수정 필요)
|
||||
// mailer($from_name, $from_email, $to_email, $subject, $content, 1);
|
||||
consultant_log("[Email Sent] To: {$to_email}, Subject: {$subject}");
|
||||
return true;
|
||||
}
|
||||
consultant_log("[Email Error] mail_manage/mailer.lib.php not found.", 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 💡 [연동] sms_admin 시스템을 사용하여 문자를 발송합니다.
|
||||
*/
|
||||
private function sendSms($to_phone, $content)
|
||||
{
|
||||
// sms_admin의 라이브러리 포함
|
||||
if (file_exists(G5_ADMIN_PATH . '/sms_admin/sms.lib.php')) {
|
||||
include_once(G5_ADMIN_PATH . '/sms_admin/sms.lib.php');
|
||||
|
||||
// sms_send() 함수 호출 (sms_admin의 함수명에 따라 수정 필요)
|
||||
// $result = sms_send($to_phone, $content);
|
||||
consultant_log("[SMS Sent] To: {$to_phone}, Content: {$content}");
|
||||
return true;
|
||||
}
|
||||
consultant_log("[SMS Error] sms_admin/sms.lib.php not found.", 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
/**
|
||||
* SQL 파일을 기반으로 데이터베이스 스키마를 관리(생성/업데이트)하는 범용 클래스
|
||||
*/
|
||||
class SchemaManager
|
||||
{
|
||||
private $sql_file_path;
|
||||
private $results;
|
||||
private $conn; // DB 연결 객체를 저장할 변수
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
* @param string $sql_file_path install.sql 파일의 절대 경로
|
||||
*/
|
||||
public function __construct($sql_file_path)
|
||||
{
|
||||
global $g5; // 그누보드 DB 연결 객체에 접근하기 위해 global 선언
|
||||
$this->conn = $g5['connect_db']; // DB 연결 객체를 저장
|
||||
|
||||
if (!file_exists($sql_file_path)) {
|
||||
throw new Exception($sql_file_path . ' 파일을 찾을 수 없습니다.');
|
||||
}
|
||||
$this->sql_file_path = $sql_file_path;
|
||||
$this->results = [
|
||||
'created' => [],
|
||||
'existing' => [],
|
||||
'updated' => [],
|
||||
'failed' => [],
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스키마 설치/업데이트를 실행합니다.
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$sql_statements = $this->parse_sql_file();
|
||||
|
||||
foreach ($sql_statements as $stmt) {
|
||||
// CREATE TABLE 문인지 확인
|
||||
if (preg_match('/^CREATE\s+TABLE/i', $stmt)) {
|
||||
$schema = $this->parse_create_table_sql($stmt);
|
||||
if ($schema && !empty($schema['name'])) {
|
||||
$this->process_table_schema($stmt, $schema);
|
||||
}
|
||||
} else {
|
||||
// CREATE TABLE 문이 아닌 다른 SQL 문 (e.g. INSERT, UPDATE)
|
||||
mysqli_query($this->conn, $stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 처리 결과를 반환합니다.
|
||||
* @return array
|
||||
*/
|
||||
public function get_results()
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 스키마를 처리합니다. (생성 또는 업데이트)
|
||||
* @param string $create_sql 전체 CREATE TABLE 구문
|
||||
* @param array $schema 파싱된 스키마 정보
|
||||
*/
|
||||
private function process_table_schema($create_sql, $schema)
|
||||
{
|
||||
$table_name = $schema['name'];
|
||||
|
||||
if ($this->table_exists($table_name)) {
|
||||
// 테이블이 존재하면, 컬럼 비교 및 추가/수정
|
||||
$this->results['existing'][] = $table_name;
|
||||
$this->update_table_columns($table_name, $schema['columns']);
|
||||
} else {
|
||||
// 테이블이 존재하지 않으면, 새로 생성
|
||||
if (mysqli_query($this->conn, $create_sql)) {
|
||||
$this->results['created'][] = $table_name;
|
||||
} else {
|
||||
$this->results['failed'][] = $table_name;
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블 생성 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 컬럼 구조를 업데이트합니다.
|
||||
* @param string $table_name
|
||||
* @param array $target_columns .sql 파일에 정의된 컬럼 목록
|
||||
*/
|
||||
private function update_table_columns($table_name, $target_columns)
|
||||
{
|
||||
$current_columns = $this->get_current_columns($table_name);
|
||||
$added_columns_in_table = [];
|
||||
|
||||
foreach ($target_columns as $col_name => $col_definition) {
|
||||
// 현재 테이블에 해당 컬럼이 없으면 추가
|
||||
if (!isset($current_columns[$col_name])) {
|
||||
$alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$col_name}` {$col_definition}";
|
||||
if (mysqli_query($this->conn, $alter_sql)) {
|
||||
$added_columns_in_table[] = $col_name;
|
||||
} else {
|
||||
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블에 '{$col_name}' 컬럼 추가 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
} else {
|
||||
// 💡 [핵심 수정] 컬럼이 이미 존재하면, 코멘트 등을 업데이트하기 위해 MODIFY 실행
|
||||
$alter_sql = "ALTER TABLE `{$table_name}` MODIFY COLUMN `{$col_name}` {$col_definition}";
|
||||
if (!mysqli_query($this->conn, $alter_sql)) {
|
||||
// MODIFY 실패 시 에러 기록
|
||||
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블의 '{$col_name}' 컬럼 수정 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($added_columns_in_table)) {
|
||||
$this->results['updated'][$table_name] = $added_columns_in_table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 파일을 읽고 각 구문으로 분리합니다.
|
||||
* @return array
|
||||
*/
|
||||
private function parse_sql_file()
|
||||
{
|
||||
$sql = file_get_contents($this->sql_file_path);
|
||||
// 주석 제거 (SQL 주석 '--' 와 C-style '/* ... */' 주석)
|
||||
$sql = preg_replace('/--.*/', '', $sql);
|
||||
$sql = preg_replace('!/\*.*?\*/!s', '', $sql);
|
||||
$sql = trim($sql);
|
||||
|
||||
// 세미콜론(;)을 기준으로 쿼리 분리
|
||||
return array_filter(array_map('trim', explode(';', $sql)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 존재 여부를 확인합니다.
|
||||
* @param string $table_name
|
||||
* @return bool
|
||||
*/
|
||||
private function table_exists($table_name)
|
||||
{
|
||||
$res = mysqli_query($this->conn, "SHOW TABLES LIKE '{$table_name}'");
|
||||
return mysqli_num_rows($res) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 DB에 있는 테이블의 컬럼 목록을 가져옵니다.
|
||||
* @param string $table_name
|
||||
* @return array
|
||||
*/
|
||||
private function get_current_columns($table_name)
|
||||
{
|
||||
$res = mysqli_query($this->conn, "SHOW COLUMNS FROM `{$table_name}`");
|
||||
$columns = [];
|
||||
while ($row = mysqli_fetch_array($res)) {
|
||||
$columns[$row['Field']] = true;
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE SQL 구문에서 테이블명과 컬럼 정의를 파싱합니다.
|
||||
* @param string $query CREATE TABLE 구문
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_create_table_sql($query)
|
||||
{
|
||||
$table_name = '';
|
||||
if (preg_match('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?(\w+)`?/i', $query, $matches)) {
|
||||
$table_name = $matches[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 괄호 안의 내용만 추출
|
||||
$start = strpos($query, '(');
|
||||
$end = strrpos($query, ')');
|
||||
if ($start === false || $end === false) {
|
||||
return ['name' => $table_name, 'columns' => []];
|
||||
}
|
||||
$content = substr($query, $start + 1, $end - $start - 1);
|
||||
|
||||
// 줄 단위로 분리
|
||||
$lines = explode("\n", $content);
|
||||
|
||||
$columns = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line, " \t\n\r\0\x0B,"); // 양쪽 공백과 마지막 쉼표 제거
|
||||
|
||||
// 컬럼 정의 라인인지 확인 (첫 단어가 `column_name` 형태)
|
||||
if (preg_match('/^`(\w+)`\s+(.*)/i', $line, $match)) {
|
||||
$col_name = $match[1];
|
||||
$col_definition = $match[2];
|
||||
$columns[$col_name] = $col_definition;
|
||||
}
|
||||
}
|
||||
|
||||
return ['name' => $table_name, 'columns' => $columns];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 로그 조회 페이지
|
||||
*/
|
||||
$sub_menu = '850630';// 새로운 메뉴 코드
|
||||
include_once('./_common.php');
|
||||
|
||||
// 권한 확인
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$g5['title'] = '시스템 로그 조회';
|
||||
|
||||
// 페이징 설정
|
||||
$page = (int)($_GET['page'] ?? 1);
|
||||
$page_rows = 20; // 페이지당 로그 수
|
||||
|
||||
$sql_common = " FROM consultant_log ";
|
||||
$sql_order = " ORDER BY log_time DESC ";
|
||||
|
||||
// 전체 로그 수
|
||||
$row = sql_fetch(" SELECT COUNT(*) AS cnt " . $sql_common);
|
||||
$total_count = $row['cnt'];
|
||||
$total_page = ceil($total_count / $page_rows);
|
||||
$from_record = ($page - 1) * $page_rows;
|
||||
|
||||
// 로그 조회
|
||||
$sql = " SELECT * " . $sql_common . $sql_order . " LIMIT {$from_record}, {$page_rows} ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.log-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.log-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.log-table th, .log-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.log-table th {
|
||||
background-color: #fff;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.log-table td.log-message {
|
||||
word-break: break-all;
|
||||
}
|
||||
.log-level {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.log-level.info { background-color: #17a2b8; }
|
||||
.log-level.warning { background-color: #ffc107; color: #212529; }
|
||||
.log-level.error { background-color: #dc3545; }
|
||||
.log-table tr:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
.text-center { text-align: center !important; }
|
||||
</style>
|
||||
|
||||
<div class="log-container">
|
||||
<div class="local_ov01 local_ov">
|
||||
<span class="btn_ov01"><span class="ov_txt">전체 로그</span><span class="ov_num"> <?php echo number_format($total_count) ?>건</span></span>
|
||||
</div>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table class="log-table">
|
||||
<caption><?php echo $g5['title']; ?></caption>
|
||||
<colgroup>
|
||||
<col style="width: 180px;">
|
||||
<col style="width: 120px;">
|
||||
<col style="width: 100px;">
|
||||
<col>
|
||||
<col style="width: 150px;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">기록 시간</th>
|
||||
<th scope="col">관리자 ID</th>
|
||||
<th scope="col">로그 종류</th>
|
||||
<th scope="col">내용</th>
|
||||
<th scope="col">IP 주소</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$log_level_class = strtolower($row['log_level']);
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center"><?php echo $row['log_time']; ?></td>
|
||||
<td class="text-center"><?php echo htmlspecialchars($row['mb_id'] ?: '비회원/시스템'); ?></td>
|
||||
<td class="text-center">
|
||||
<span class="log-level <?php echo $log_level_class; ?>">
|
||||
<?php echo ucfirst($log_level_class); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="log-message"><?php echo htmlspecialchars($row['log_message']); ?></td>
|
||||
<td class="text-center"><?php echo $row['ip_address']; ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
if ($i == 0) {
|
||||
echo '<tr><td colspan="5" class="empty_table">데이터가 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['PHP_SELF'].'?'.$qstr.'&page='); ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-
|
||||
@@ -0,0 +1,500 @@
|
||||
<?php
|
||||
/**
|
||||
* 예약 관리
|
||||
*/
|
||||
$sub_menu = '850200';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '예약 관리';
|
||||
|
||||
// 필터 파라미터
|
||||
$status = $_GET['status'] ?? '';
|
||||
$date = $_GET['date'] ?? '';
|
||||
$search = $_GET['search'] ?? '';
|
||||
$page = (int)($_GET['page'] ?? 1);
|
||||
$per_page = 20;
|
||||
|
||||
// 상태 변경 처리
|
||||
if ($_POST['action'] == 'update_status') {
|
||||
$reservation_id = (int)$_POST['reservation_id'];
|
||||
$new_status = $_POST['new_status'];
|
||||
$memo = $_POST['memo'] ?? '';
|
||||
$send_sms = isset($_POST['send_sms']) && $_POST['send_sms'] == '1';
|
||||
$send_email = isset($_POST['send_email']) && $_POST['send_email'] == '1';
|
||||
|
||||
if ($reservation_id && $new_status) {
|
||||
// 기존 상태 조회
|
||||
$old_res = sql_fetch("SELECT * FROM consultant_reservations WHERE id = {$reservation_id}");
|
||||
|
||||
$sql = "UPDATE consultant_reservations
|
||||
SET status = '" . sql_real_escape_string($new_status) . "',
|
||||
admin_memo = '" . sql_real_escape_string($memo) . "',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$reservation_id}";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
// 💡 [추가] 알림 발송 로직
|
||||
if ($send_sms || $send_email) {
|
||||
// 템플릿 키 결정
|
||||
$template_key = '';
|
||||
if ($new_status == 'reserved') {
|
||||
$template_key = 'consultant_confirmed_customer';
|
||||
} elseif ($new_status == 'cancelled') {
|
||||
$template_key = 'consultant_cancelled_customer';
|
||||
}
|
||||
|
||||
if ($template_key) {
|
||||
// 알림 데이터 준비
|
||||
$noti_data = [
|
||||
'customer_name' => $old_res['customer_name'],
|
||||
'customer_phone' => $old_res['customer_phone'],
|
||||
'customer_email' => $old_res['customer_email'],
|
||||
'reservation_date' => $old_res['reservation_date'],
|
||||
'reservation_time' => substr($old_res['reservation_time'], 0, 5),
|
||||
'payment_amount' => number_format($old_res['payment_amount']),
|
||||
'cancel_reason' => $memo // 취소 사유로 메모 사용
|
||||
];
|
||||
|
||||
if ($send_sms) {
|
||||
consultant_send_notification('sms', $template_key, $noti_data);
|
||||
}
|
||||
if ($send_email) {
|
||||
consultant_send_notification('email', $template_key, $noti_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alert('상태가 변경되었습니다.', $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING']);
|
||||
} else {
|
||||
alert('상태 변경에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 검색 조건 구성
|
||||
$where_conditions = ["is_deleted = 0"];
|
||||
|
||||
if ($status) {
|
||||
$where_conditions[] = "status = '" . sql_real_escape_string($status) . "'";
|
||||
}
|
||||
|
||||
if ($date) {
|
||||
$where_conditions[] = "reservation_date = '" . sql_real_escape_string($date) . "'";
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search_escaped = sql_real_escape_string($search);
|
||||
$where_conditions[] = "(customer_name LIKE '%{$search_escaped}%' OR customer_phone LIKE '%{$search_escaped}%')";
|
||||
}
|
||||
|
||||
$where_clause = implode(' AND ', $where_conditions);
|
||||
|
||||
// 전체 개수 조회
|
||||
$count_sql = "SELECT COUNT(*) as total FROM consultant_reservations 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 consultant_reservations
|
||||
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;
|
||||
}
|
||||
|
||||
// 상태 라벨
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.reservations-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reservations-table {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-success { background: #28a745; color: white; }
|
||||
.btn-warning { background: #ffc107; color: #212529; }
|
||||
.btn-danger { background: #dc3545; color: white; }
|
||||
.btn-secondary { background: #6c757d; color: white; }
|
||||
.btn-info { background: #17a2b8; color: white; } /* 견적작성 버튼용 */
|
||||
|
||||
.form-control {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination .current {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="reservations-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<!-- 필터 폼 -->
|
||||
<form method="get" class="filter-form">
|
||||
<select name="status" class="form-control">
|
||||
<option value="">전체 상태</option>
|
||||
<?php foreach ($status_labels as $key => $label): ?>
|
||||
<option value="<?php echo $key; ?>" <?php echo $status == $key ? 'selected' : ''; ?>>
|
||||
<?php echo $label; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<input type="date" name="date" value="<?php echo $date; ?>" class="form-control" placeholder="예약일">
|
||||
|
||||
<input type="text" name="search" value="<?php echo htmlspecialchars($search); ?>"
|
||||
class="form-control" placeholder="고객명 또는 연락처 검색">
|
||||
|
||||
<button type="submit" class="btn btn-primary">검색</button>
|
||||
<a href="<?php echo $_SERVER['PHP_SELF']; ?>" class="btn btn-secondary">초기화</a>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</form>
|
||||
|
||||
<!-- 예약 목록 -->
|
||||
<div class="reservations-table">
|
||||
<div class="table-header">
|
||||
<span>예약 목록 (총 <?php echo number_format($total); ?>건)</span>
|
||||
<span>페이지 <?php echo $page; ?> / <?php echo $total_pages; ?></span>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($reservations)): ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>예약번호</th>
|
||||
<th>고객정보</th>
|
||||
<th>예약일시</th>
|
||||
<th>상담유형</th>
|
||||
<th>상담비</th>
|
||||
<th>상태</th>
|
||||
<th>신청일</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($reservations as $reservation): ?>
|
||||
<tr>
|
||||
<td>#<?php echo $reservation['id']; ?></td>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($reservation['customer_name']); ?></strong><br>
|
||||
<small><?php echo htmlspecialchars($reservation['customer_phone']); ?></small><br>
|
||||
<small><?php echo htmlspecialchars($reservation['customer_email']); ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo $reservation['reservation_date']; ?><br>
|
||||
<small><?php echo substr($reservation['reservation_time'], 0, 5); ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$types = ['onsite' => '현장', 'online' => '온라인', 'phone' => '전화'];
|
||||
echo $types[$reservation['consultation_type']] ?? $reservation['consultation_type'];
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo number_format($reservation['payment_amount']); ?>원</td>
|
||||
<td>
|
||||
<span class="status-badge status-<?php echo $reservation['status']; ?>">
|
||||
<?php echo $status_labels[$reservation['status']] ?? $reservation['status']; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo date('m-d H:i', strtotime($reservation['created_at'])); ?></td>
|
||||
<td>
|
||||
<button onclick="openStatusModal(<?php echo $reservation['id']; ?>, '<?php echo $reservation['status']; ?>')" class="btn btn-warning btn-sm">상태변경</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="no-data">
|
||||
검색 조건에 맞는 예약이 없습니다.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 페이징 -->
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page - 1])); ?>">이전</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$start_page = max(1, $page - 5);
|
||||
$end_page = min($total_pages, $page + 5);
|
||||
|
||||
for ($i = $start_page; $i <= $end_page; $i++):
|
||||
?>
|
||||
<?php if ($i == $page): ?>
|
||||
<span class="current"><?php echo $i; ?></span>
|
||||
<?php else: ?>
|
||||
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $i])); ?>"><?php echo $i; ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page + 1])); ?>">다음</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 상태 변경 모달 -->
|
||||
<div id="statusModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h3>예약 상태 변경</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="update_status">
|
||||
<input type="hidden" name="reservation_id" id="modal_reservation_id">
|
||||
|
||||
<div style="margin: 15px 0;">
|
||||
<label for="modal_new_status">변경할 상태</label>
|
||||
<select name="new_status" id="modal_new_status" class="form-control" required>
|
||||
<?php foreach ($status_labels as $key => $label): ?>
|
||||
<option value="<?php echo $key; ?>"><?php echo $label; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin: 15px 0;">
|
||||
<label for="modal_memo">관리자 메모 (선택)</label>
|
||||
<textarea name="memo" rows="3" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [추가] 알림 발송 옵션 -->
|
||||
<div style="margin: 15px 0; background: #f8f9fa; padding: 10px; border-radius: 4px;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">알림 발송</div>
|
||||
<label style="margin-right: 15px;">
|
||||
<input type="checkbox" name="send_sms" value="1" checked> 문자(SMS) 발송
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="send_email" value="1" checked> 이메일 발송
|
||||
</label>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||
* 예약확정/취소 상태 변경 시에만 발송됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-primary">확인</button>
|
||||
<button type="button" onclick="closeModal()" class="btn btn-secondary">취소</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openStatusModal(reservationId, currentStatus) {
|
||||
document.getElementById('modal_reservation_id').value = reservationId;
|
||||
document.getElementById('modal_new_status').value = currentStatus;
|
||||
document.getElementById('statusModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('statusModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 모달 외부 클릭시 닫기
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('statusModal');
|
||||
if (event.target == modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 닫기 버튼
|
||||
document.querySelector('.close').onclick = function() {
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 리소스(상담사/공간) 관리
|
||||
*/
|
||||
$sub_menu = '850615';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
$g5['title'] = '리소스(상담사) 관리';
|
||||
|
||||
// --- 액션 처리 ---
|
||||
|
||||
// 그룹 추가/수정
|
||||
if ($_POST['action'] == 'save_group') {
|
||||
$group_name = trim($_POST['group_name']);
|
||||
$group_id = (int)$_POST['group_id'];
|
||||
|
||||
if ($group_name) {
|
||||
if ($group_id > 0) {
|
||||
$sql = "UPDATE consultant_groups SET name = '{$group_name}' WHERE id = {$group_id}";
|
||||
} else {
|
||||
$sql = "INSERT INTO consultant_groups (name, is_active) VALUES ('{$group_name}', 1)";
|
||||
}
|
||||
sql_query($sql);
|
||||
alert('저장되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// 그룹 삭제
|
||||
if ($_POST['action'] == 'delete_group') {
|
||||
$group_id = (int)$_POST['group_id'];
|
||||
// 리소스가 있는지 확인
|
||||
$cnt = sql_fetch("SELECT COUNT(*) as cnt FROM consultant_resources WHERE group_id = {$group_id}")['cnt'];
|
||||
if ($cnt > 0) {
|
||||
alert('해당 그룹에 속한 리소스가 있어 삭제할 수 없습니다.');
|
||||
} else {
|
||||
sql_query("DELETE FROM consultant_groups WHERE id = {$group_id}");
|
||||
alert('삭제되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// 리소스 추가/수정
|
||||
if ($_POST['action'] == 'save_resource') {
|
||||
$resource_id = (int)$_POST['resource_id'];
|
||||
$group_id = (int)$_POST['group_id'];
|
||||
$name = trim($_POST['resource_name']);
|
||||
$desc = trim($_POST['resource_desc']);
|
||||
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if ($name && $group_id) {
|
||||
if ($resource_id > 0) {
|
||||
$sql = "UPDATE consultant_resources
|
||||
SET group_id = {$group_id}, name = '{$name}', description = '{$desc}', is_active = {$is_active}
|
||||
WHERE id = {$resource_id}";
|
||||
} else {
|
||||
$sql = "INSERT INTO consultant_resources
|
||||
(group_id, name, description, is_active)
|
||||
VALUES ({$group_id}, '{$name}', '{$desc}', {$is_active})";
|
||||
}
|
||||
sql_query($sql);
|
||||
alert('저장되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// 리소스 삭제
|
||||
if ($_POST['action'] == 'delete_resource') {
|
||||
$resource_id = (int)$_POST['resource_id'];
|
||||
// 예약 내역 확인 (안전 삭제)
|
||||
$cnt = sql_fetch("SELECT COUNT(*) as cnt FROM consultant_reservations WHERE resource_id = {$resource_id}")['cnt'];
|
||||
if ($cnt > 0) {
|
||||
// 실제 삭제 대신 비활성화 처리 권장하지만, 여기서는 경고 후 삭제 방지
|
||||
alert('해당 리소스로 접수된 예약이 있어 삭제할 수 없습니다. 대신 사용 안 함으로 설정해주세요.');
|
||||
} else {
|
||||
sql_query("DELETE FROM consultant_resources WHERE id = {$resource_id}");
|
||||
alert('삭제되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 데이터 조회 ---
|
||||
$groups = [];
|
||||
$result = sql_query("SELECT * FROM consultant_groups ORDER BY id");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$groups[$row['id']] = $row;
|
||||
}
|
||||
|
||||
$resources = [];
|
||||
$result = sql_query("SELECT r.*, g.name as group_name
|
||||
FROM consultant_resources r
|
||||
LEFT JOIN consultant_groups g ON r.group_id = g.id
|
||||
ORDER BY r.group_id, r.id");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$resources[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.resource-container { max-width: 1200px; margin: 0 auto; padding: 20px; display: grid; grid-template-columns: 300px 1fr; gap: 20px; }
|
||||
.card { background: #fff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }
|
||||
.card-header { padding: 15px; background: #f8f9fa; border-bottom: 1px solid #ddd; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-body { padding: 20px; }
|
||||
|
||||
.group-list { list-style: none; padding: 0; margin: 0; }
|
||||
.group-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
||||
.group-item:last-child { border-bottom: none; }
|
||||
.group-item:hover { background: #f1f3f5; }
|
||||
|
||||
.resource-table { width: 100%; border-collapse: collapse; }
|
||||
.resource-table th, .resource-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
.resource-table th { background: #f8f9fa; font-weight: bold; }
|
||||
|
||||
.btn-xs { padding: 2px 6px; font-size: 11px; border-radius: 3px; }
|
||||
.badge { padding: 3px 8px; border-radius: 10px; font-size: 11px; color: #fff; }
|
||||
.badge-success { background: #28a745; }
|
||||
.badge-secondary { background: #6c757d; }
|
||||
|
||||
.form-control { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.resource-container { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="resource-container">
|
||||
<!-- 좌측: 그룹 관리 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>그룹 관리</span>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="openGroupModal()">+ 추가</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<ul class="group-list">
|
||||
<?php if (empty($groups)): ?>
|
||||
<li class="group-item" style="justify-content:center; color:#999;">등록된 그룹이 없습니다.</li>
|
||||
<?php else: ?>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<li class="group-item">
|
||||
<span><?php echo htmlspecialchars($group['name']); ?></span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary btn-xs" onclick="openGroupModal(<?php echo $group['id']; ?>, '<?php echo htmlspecialchars($group['name']); ?>')">수정</button>
|
||||
<button type="button" class="btn btn-danger btn-xs" onclick="deleteGroup(<?php echo $group['id']; ?>)">삭제</button>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 리소스 목록 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>리소스(상담사/공간) 목록</span>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="openResourceModal()">+ 리소스 추가</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<table class="resource-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>그룹</th>
|
||||
<th>이름</th>
|
||||
<th>설명</th>
|
||||
<th>상태</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($resources)): ?>
|
||||
<tr><td colspan="5" style="text-align:center; padding:30px; color:#999;">등록된 리소스가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($resources as $res): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($res['group_name']); ?></td>
|
||||
<td><strong><?php echo htmlspecialchars($res['name']); ?></strong></td>
|
||||
<td><?php echo htmlspecialchars($res['description']); ?></td>
|
||||
<td>
|
||||
<?php if ($res['is_active']): ?>
|
||||
<span class="badge badge-success">사용중</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">중지</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-secondary btn-xs"
|
||||
onclick="openResourceModal(<?php echo $res['id']; ?>, <?php echo $res['group_id']; ?>, '<?php echo htmlspecialchars($res['name']); ?>', '<?php echo htmlspecialchars($res['description']); ?>', <?php echo $res['is_active']; ?>)">
|
||||
수정
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-xs" onclick="deleteResource(<?php echo $res['id']; ?>)">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 그룹 추가/수정 모달 -->
|
||||
<div id="groupModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="width:350px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="groupModalTitle">그룹 추가</h3>
|
||||
<span class="close" onclick="closeModal('groupModal')">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="save_group">
|
||||
<input type="hidden" name="group_id" id="modal_group_id" value="0">
|
||||
<div class="form-group">
|
||||
<label>그룹명 (예: 내과, 회의실)</label>
|
||||
<input type="text" name="group_name" id="modal_group_name" class="form-control" required>
|
||||
</div>
|
||||
<div style="text-align:right; margin-top:15px;">
|
||||
<button type="submit" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 리소스 추가/수정 모달 -->
|
||||
<div id="resourceModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="resourceModalTitle">리소스 추가</h3>
|
||||
<span class="close" onclick="closeModal('resourceModal')">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="save_resource">
|
||||
<input type="hidden" name="resource_id" id="modal_res_id" value="0">
|
||||
|
||||
<div class="form-group">
|
||||
<label>그룹 선택</label>
|
||||
<select name="group_id" id="modal_res_group" class="form-control" required>
|
||||
<option value="">선택하세요</option>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<option value="<?php echo $group['id']; ?>"><?php echo htmlspecialchars($group['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>이름 (예: 김의사, A회의실)</label>
|
||||
<input type="text" name="resource_name" id="modal_res_name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>설명 (선택)</label>
|
||||
<input type="text" name="resource_desc" id="modal_res_desc" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" name="is_active" id="modal_res_active" value="1" checked> 사용함</label>
|
||||
</div>
|
||||
|
||||
<div style="text-align:right; margin-top:15px;">
|
||||
<button type="submit" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 삭제 폼 (히든) -->
|
||||
<form id="deleteForm" method="post">
|
||||
<input type="hidden" name="action" id="deleteAction">
|
||||
<input type="hidden" name="group_id" id="deleteGroupId">
|
||||
<input type="hidden" name="resource_id" id="deleteResourceId">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function openGroupModal(id = 0, name = '') {
|
||||
document.getElementById('groupModalTitle').textContent = id ? '그룹 수정' : '그룹 추가';
|
||||
document.getElementById('modal_group_id').value = id;
|
||||
document.getElementById('modal_group_name').value = name;
|
||||
document.getElementById('groupModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function openResourceModal(id = 0, groupId = '', name = '', desc = '', active = 1) {
|
||||
document.getElementById('resourceModalTitle').textContent = id ? '리소스 수정' : '리소스 추가';
|
||||
document.getElementById('modal_res_id').value = id;
|
||||
document.getElementById('modal_res_group').value = groupId;
|
||||
document.getElementById('modal_res_name').value = name;
|
||||
document.getElementById('modal_res_desc').value = desc;
|
||||
document.getElementById('modal_res_active').checked = (active == 1);
|
||||
document.getElementById('resourceModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).style.display = 'none';
|
||||
}
|
||||
|
||||
function deleteGroup(id) {
|
||||
if(confirm('정말 삭제하시겠습니까?')) {
|
||||
document.getElementById('deleteAction').value = 'delete_group';
|
||||
document.getElementById('deleteGroupId').value = id;
|
||||
document.getElementById('deleteForm').submit();
|
||||
}
|
||||
}
|
||||
|
||||
function deleteResource(id) {
|
||||
if(confirm('정말 삭제하시겠습니까?')) {
|
||||
document.getElementById('deleteAction').value = 'delete_resource';
|
||||
document.getElementById('deleteResourceId').value = id;
|
||||
document.getElementById('deleteForm').submit();
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 닫기
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
$sub_menu = '850500'; // 💡 메뉴 코드 수정
|
||||
include_once('./_common.php');
|
||||
|
||||
$g5['title'] = '상담 예약 팝업 샘플';
|
||||
include_once(G5_ADMIN_PATH.'/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
이 페이지는 사이트의 어떤 페이지에서든 상담 예약 기능을 쉽게 추가하는 방법을 보여주는 예제입니다.<br>
|
||||
아래 코드 한 줄만 포함하면, 버튼과 팝업 기능이 모두 활성화됩니다.
|
||||
</p>
|
||||
<pre><code><?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?></code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [시작] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<div style="text-align:center; padding: 50px 20px; background-color:#f5f5f5; border-radius:10px; margin: 20px 0;">
|
||||
<h3 style="margin-bottom:15px;">상담이 필요하신가요?</h3>
|
||||
<p style="margin-bottom:25px; color:#666;">버튼을 눌러 간편하게 상담을 신청하거나, 기존 예약을 확인/취소할 수 있습니다.</p>
|
||||
|
||||
<!-- 1. 팝업을 여는 버튼들 -->
|
||||
<div class="consultant-buttons">
|
||||
<button type="button" class="reservation-btn-main" onclick="openReservationPopup()">
|
||||
<i class="fa fa-calendar"></i> 상담 예약 신청
|
||||
</button>
|
||||
<button type="button" class="reservation-check-btn" onclick="openReservationCheckPopup()">
|
||||
예약 확인/취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 2. 팝업 파일들 포함 (이 코드 한 줄이면 모든 팝업 기능이 로드됩니다)
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php');
|
||||
?>
|
||||
|
||||
<!-- 버튼 디자인을 위한 CSS (사이트의 공통 CSS 파일에 추가하는 것을 권장합니다) -->
|
||||
<style>
|
||||
.consultant-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.reservation-btn-main, .reservation-check-btn {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.reservation-btn-main {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
.reservation-btn-main:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
.reservation-btn-main i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.reservation-check-btn {
|
||||
background: #6c757d;
|
||||
}
|
||||
.reservation-check-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [끝] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH.'/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 일정 설정
|
||||
*/
|
||||
$sub_menu = '850300';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 일정 설정';
|
||||
|
||||
// 월별 스케줄 생성 처리
|
||||
if ($_POST['action'] == 'generate_monthly_schedule') {
|
||||
try {
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
$generator = new ScheduleGenerator();
|
||||
|
||||
$year = (int)($_POST['year'] ?? date('Y'));
|
||||
$month = (int)($_POST['month'] ?? date('n'));
|
||||
|
||||
// 충돌 검사
|
||||
$conflicts = $generator->checkScheduleConflicts($year, $month);
|
||||
if (!empty($conflicts)) {
|
||||
$conflictMsg = "다음 예약과 충돌이 발생합니다:\\n";
|
||||
foreach ($conflicts as $conflict) {
|
||||
$conflictMsg .= "- {$conflict['date']} {$conflict['time']} {$conflict['customer']} ({$conflict['phone']})\\n";
|
||||
}
|
||||
$conflictMsg .= "\\n계속 진행하시겠습니까?";
|
||||
|
||||
if (!isset($_POST['force_generate'])) {
|
||||
echo "<script>
|
||||
if (confirm('{$conflictMsg}')) {
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.innerHTML = '<input type=\"hidden\" name=\"action\" value=\"generate_monthly_schedule\">' +
|
||||
'<input type=\"hidden\" name=\"year\" value=\"{$year}\">' +
|
||||
'<input type=\"hidden\" name=\"month\" value=\"{$month}\">' +
|
||||
'<input type=\"hidden\" name=\"force_generate\" value=\"1\">';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
</script>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 스케줄 생성
|
||||
if ($generator->generateMonth($year, $month)) {
|
||||
alert("{$year}년 {$month}월 스케줄이 생성되었습니다.", $_SERVER['PHP_SELF']);
|
||||
} else {
|
||||
alert("스케줄 생성에 실패했습니다.");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert("오류가 발생했습니다: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 폼 처리 (기존 코드 유지)
|
||||
if ($_POST['action'] == 'save_schedule') {
|
||||
// 기존 스케줄 삭제
|
||||
sql_query("DELETE FROM consultant_schedule WHERE day_of_week IS NOT NULL");
|
||||
|
||||
// 새 스케줄 저장
|
||||
for ($day = 1; $day <= 7; $day++) {
|
||||
$enabled = $_POST["day_{$day}_enabled"] ?? 0;
|
||||
$start_time = $_POST["day_{$day}_start"] ?? '09:00';
|
||||
$end_time = $_POST["day_{$day}_end"] ?? '18:00';
|
||||
|
||||
if ($enabled) {
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(day_of_week, start_time, end_time, time_slot, max_persons, is_available)
|
||||
VALUES ({$day}, '{$start_time}', '{$end_time}', 60, 2, 1)";
|
||||
sql_query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
alert('일정이 저장되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
|
||||
// 현재 스케줄 조회
|
||||
$schedules = [];
|
||||
$sql = "SELECT * FROM consultant_schedule WHERE day_of_week IS NOT NULL ORDER BY day_of_week";
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$schedules[$row['day_of_week']] = $row;
|
||||
}
|
||||
|
||||
$days = ['', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.schedule-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.schedule-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #ddd;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 16px 28px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monthly-generator {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.generator-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #856404;
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
.schedule-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.schedule-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
}
|
||||
.day-schedule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.day-schedule:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.day-name {
|
||||
width: 100px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.day-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
.time-input {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 100px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="schedule-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>안내:</strong> 상담 가능한 요일과 시간을 설정하세요. 체크된 요일만 예약이 가능합니다.
|
||||
</div>
|
||||
|
||||
<form method="post" class="schedule-form">
|
||||
<input type="hidden" name="action" value="save_schedule">
|
||||
|
||||
<?php for ($day = 1; $day <= 7; $day++): ?>
|
||||
<?php
|
||||
$schedule = $schedules[$day] ?? null;
|
||||
$enabled = $schedule ? 1 : 0;
|
||||
$start_time = $schedule['start_time'] ?? '09:00';
|
||||
$end_time = $schedule['end_time'] ?? '18:00';
|
||||
?>
|
||||
|
||||
<div class="day-schedule">
|
||||
<div class="day-name"><?php echo $days[$day]; ?></div>
|
||||
<div class="day-controls">
|
||||
<label>
|
||||
<input type="checkbox" name="day_<?php echo $day; ?>_enabled" value="1"
|
||||
<?php echo $enabled ? 'checked' : ''; ?>>
|
||||
운영
|
||||
</label>
|
||||
|
||||
<label>
|
||||
시작:
|
||||
<input type="time" name="day_<?php echo $day; ?>_start"
|
||||
value="<?php echo $start_time; ?>" class="time-input">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
종료:
|
||||
<input type="time" name="day_<?php echo $day; ?>_end"
|
||||
value="<?php echo $end_time; ?>" class="time-input">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<button type="submit" class="btn btn-primary">저장</button>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,454 @@
|
||||
<?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');
|
||||
?>
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
/**
|
||||
* 월별 스케줄 자동 생성 엔진
|
||||
*/
|
||||
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
class ScheduleGenerator
|
||||
{
|
||||
|
||||
/**
|
||||
* 특정 월의 전체 스케줄 생성
|
||||
*/
|
||||
public function generateMonth($year, $month)
|
||||
{
|
||||
try {
|
||||
// 기본 설정 조회
|
||||
$basic_settings = $this->getBasicSettings();
|
||||
|
||||
// 요일별 설정 조회
|
||||
$weekly_settings = $this->getWeeklySettings();
|
||||
|
||||
// 해당 월의 모든 날짜 생성
|
||||
$dates = $this->getMonthDates($year, $month);
|
||||
|
||||
// 기존 스케줄 삭제 (자동 생성된 것만)
|
||||
$this->clearAutoGeneratedSchedule($year, $month);
|
||||
|
||||
$generated_count = 0;
|
||||
|
||||
foreach ($dates as $date) {
|
||||
$day_of_week = date('w', strtotime($date)); // 0=일요일, 1=월요일, ...
|
||||
$day_name = $this->getDayName($day_of_week);
|
||||
|
||||
// 해당 요일의 설정 확인
|
||||
if (isset($weekly_settings[$day_name]) && $weekly_settings[$day_name]['enabled'] == '1') {
|
||||
// 운영일인 경우 스케줄 생성
|
||||
$slots_created = $this->generateDay($date, $weekly_settings[$day_name], $basic_settings);
|
||||
$generated_count += $slots_created;
|
||||
} else {
|
||||
// 휴무일인 경우 휴무 표시
|
||||
$this->createHolidaySlot($date, '휴무일');
|
||||
}
|
||||
}
|
||||
|
||||
consultant_log("월별 스케줄 생성 완료: {$year}-{$month}, 생성된 슬롯: {$generated_count}개");
|
||||
return $generated_count;
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("월별 스케줄 생성 실패: " . $e->getMessage(), 'error');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜의 스케줄 생성
|
||||
*/
|
||||
public function generateDay($date, $day_settings, $basic_settings)
|
||||
{
|
||||
$slots_created = 0;
|
||||
|
||||
try {
|
||||
$start_time = $day_settings['start'];
|
||||
$end_time = $day_settings['end'];
|
||||
$lunch_start = $day_settings['lunch_start'];
|
||||
$lunch_end = $day_settings['lunch_end'];
|
||||
|
||||
$slot_duration = (int) $basic_settings['consultation_duration'];
|
||||
$max_persons = (int) $basic_settings['max_persons_per_slot'];
|
||||
|
||||
// 시간 슬롯 생성
|
||||
$current_time = strtotime($start_time);
|
||||
$end_timestamp = strtotime($end_time);
|
||||
|
||||
while ($current_time < $end_timestamp) {
|
||||
$slot_start = date('H:i', $current_time);
|
||||
$slot_end = date('H:i', $current_time + ($slot_duration * 60));
|
||||
|
||||
// 종료시간이 운영시간을 넘지 않도록 체크
|
||||
if (strtotime($slot_end) > $end_timestamp) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 점심시간 체크
|
||||
$is_lunch_time = $this->isLunchTime($slot_start, $slot_end, $lunch_start, $lunch_end);
|
||||
|
||||
if ($is_lunch_time) {
|
||||
// 점심시간 슬롯 생성
|
||||
$this->createTimeSlot($date, $slot_start, $slot_end, $slot_duration, 0, 0, 'lunch_time');
|
||||
} else {
|
||||
// 일반 상담 슬롯 생성
|
||||
$this->createTimeSlot($date, $slot_start, $slot_end, $slot_duration, $max_persons, 1, 'auto_generated');
|
||||
}
|
||||
|
||||
$slots_created++;
|
||||
$current_time += ($slot_duration * 60);
|
||||
}
|
||||
|
||||
return $slots_created;
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("일별 스케줄 생성 실패 ({$date}): " . $e->getMessage(), 'error');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간 슬롯 생성
|
||||
*/
|
||||
private function createTimeSlot($date, $start_time, $end_time, $duration, $max_persons, $is_available, $type)
|
||||
{
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, created_at)
|
||||
VALUES
|
||||
('{$date}', '{$start_time}', '{$end_time}', {$duration}, {$max_persons}, {$is_available}, '{$type}', NOW())";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴무일 슬롯 생성
|
||||
*/
|
||||
private function createHolidaySlot($date, $reason = '휴무일')
|
||||
{
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, temp_2, created_at)
|
||||
VALUES
|
||||
('{$date}', '00:00', '23:59', 0, 0, 0, 'holiday', '{$reason}', NOW())";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 점심시간 여부 확인
|
||||
*/
|
||||
private function isLunchTime($slot_start, $slot_end, $lunch_start, $lunch_end)
|
||||
{
|
||||
if (empty($lunch_start) || empty($lunch_end)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$slot_start_time = strtotime($slot_start);
|
||||
$slot_end_time = strtotime($slot_end);
|
||||
$lunch_start_time = strtotime($lunch_start);
|
||||
$lunch_end_time = strtotime($lunch_end);
|
||||
|
||||
// 슬롯이 점심시간과 겹치는지 확인
|
||||
return ($slot_start_time >= $lunch_start_time && $slot_start_time < $lunch_end_time) ||
|
||||
($slot_end_time > $lunch_start_time && $slot_end_time <= $lunch_end_time) ||
|
||||
($slot_start_time <= $lunch_start_time && $slot_end_time >= $lunch_end_time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 월의 모든 날짜 배열 생성
|
||||
*/
|
||||
private function getMonthDates($year, $month)
|
||||
{
|
||||
$dates = [];
|
||||
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
|
||||
|
||||
for ($day = 1; $day <= $days_in_month; $day++) {
|
||||
$dates[] = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일 숫자를 요일명으로 변환
|
||||
*/
|
||||
private function getDayName($day_of_week)
|
||||
{
|
||||
$day_names = [
|
||||
0 => 'sunday',
|
||||
1 => 'monday',
|
||||
2 => 'tuesday',
|
||||
3 => 'wednesday',
|
||||
4 => 'thursday',
|
||||
5 => 'friday',
|
||||
6 => 'saturday'
|
||||
];
|
||||
|
||||
return $day_names[$day_of_week] ?? 'sunday';
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 설정 조회
|
||||
*/
|
||||
private function getBasicSettings()
|
||||
{
|
||||
return [
|
||||
'consultation_duration' => consultant_get_config('consultation_duration', '60'),
|
||||
'max_persons_per_slot' => consultant_get_config('max_persons_per_slot', '2'),
|
||||
'consultation_fee' => consultant_get_config('consultation_fee', '50000')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일별 설정 조회
|
||||
*/
|
||||
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'),
|
||||
'start' => consultant_get_config($day . '_start', '09:00'),
|
||||
'end' => consultant_get_config($day . '_end', '18:00'),
|
||||
'lunch_start' => consultant_get_config($day . '_lunch_start', '12:00'),
|
||||
'lunch_end' => consultant_get_config($day . '_lunch_end', '13:00')
|
||||
];
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 생성된 스케줄 삭제
|
||||
*/
|
||||
private function clearAutoGeneratedSchedule($year, $month)
|
||||
{
|
||||
$start_date = sprintf('%04d-%02d-01', $year, $month);
|
||||
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
|
||||
|
||||
// 기존 예약이 없는 자동 생성 스케줄만 삭제
|
||||
$sql = "DELETE cs FROM consultant_schedule cs
|
||||
LEFT JOIN consultant_reservations cr ON (
|
||||
cs.specific_date = cr.reservation_date
|
||||
AND cs.start_time = cr.reservation_time
|
||||
AND cr.is_deleted = 0
|
||||
)
|
||||
WHERE cs.specific_date >= '{$start_date}'
|
||||
AND cs.specific_date <= '{$end_date}'
|
||||
AND cs.temp_1 IN ('auto_generated', 'lunch_time', 'holiday')
|
||||
AND cr.id IS NULL";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 예약 보호 - 예약이 있는 시간대 확인
|
||||
*/
|
||||
public function getExistingReservations($year, $month)
|
||||
{
|
||||
$start_date = sprintf('%04d-%02d-01', $year, $month);
|
||||
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
|
||||
|
||||
$sql = "SELECT reservation_date, reservation_time, COUNT(*) as count
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$start_date}'
|
||||
AND reservation_date <= '{$end_date}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_date, reservation_time";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$reservations = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$key = $row['reservation_date'] . '_' . $row['reservation_time'];
|
||||
$reservations[$key] = $row['count'];
|
||||
}
|
||||
}
|
||||
|
||||
return $reservations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 달 스케줄 자동 생성 (크론잡용)
|
||||
*/
|
||||
public function generateNextMonth()
|
||||
{
|
||||
$next_month = date('Y-m', strtotime('+1 month'));
|
||||
list($year, $month) = explode('-', $next_month);
|
||||
|
||||
return $this->generateMonth((int) $year, (int) $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 생성 상태 확인
|
||||
*/
|
||||
public function checkScheduleStatus($year, $month)
|
||||
{
|
||||
$start_date = sprintf('%04d-%02d-01', $year, $month);
|
||||
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
|
||||
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'auto_generated' THEN 1 END) as auto_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'lunch_time' THEN 1 END) as lunch_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'holiday' THEN 1 END) as holiday_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'admin_blocked' THEN 1 END) as blocked_slots
|
||||
FROM consultant_schedule
|
||||
WHERE specific_date >= '{$start_date}'
|
||||
AND specific_date <= '{$end_date}'";
|
||||
|
||||
return sql_fetch($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 변경 시 영향받는 예약 확인
|
||||
*/
|
||||
public function checkSettingConflicts($year, $month)
|
||||
{
|
||||
$existing_reservations = $this->getExistingReservations($year, $month);
|
||||
$conflicts = [];
|
||||
|
||||
// 새로운 설정으로 생성될 스케줄과 기존 예약 비교
|
||||
$weekly_settings = $this->getWeeklySettings();
|
||||
$dates = $this->getMonthDates($year, $month);
|
||||
|
||||
foreach ($dates as $date) {
|
||||
$day_of_week = date('w', strtotime($date));
|
||||
$day_name = $this->getDayName($day_of_week);
|
||||
|
||||
// 휴무일로 변경되었는데 예약이 있는 경우
|
||||
if (!isset($weekly_settings[$day_name]) || $weekly_settings[$day_name]['enabled'] != '1') {
|
||||
foreach ($existing_reservations as $key => $count) {
|
||||
if (strpos($key, $date) === 0) {
|
||||
$conflicts[] = [
|
||||
'date' => $date,
|
||||
'type' => 'holiday_conflict',
|
||||
'message' => "{$date}는 휴무일로 설정되었지만 {$count}건의 예약이 있습니다."
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 생성 헬퍼 함수들
|
||||
*/
|
||||
|
||||
/**
|
||||
* 월별 스케줄 생성 실행
|
||||
*/
|
||||
function generate_monthly_schedule($year, $month)
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->generateMonth($year, $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 달 스케줄 자동 생성
|
||||
*/
|
||||
function auto_generate_next_month_schedule()
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->generateNextMonth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 생성 상태 확인
|
||||
*/
|
||||
function get_schedule_generation_status($year, $month)
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->checkScheduleStatus($year, $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 변경 영향 확인
|
||||
*/
|
||||
function check_schedule_setting_conflicts($year, $month)
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->checkSettingConflicts($year, $month);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 통합 설정 관리
|
||||
*/
|
||||
|
||||
$sub_menu = '850610'; // 메뉴 코드 (기본/운영 설정)
|
||||
include_once('./_common.php');
|
||||
|
||||
// 권한 확인
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 예약 설정';
|
||||
|
||||
// 현재 탭 확인
|
||||
$current_tab = $_GET['tab'] ?? 'basic';
|
||||
|
||||
// 폼 처리
|
||||
if (isset($_POST['action']) && $_POST['action']) {
|
||||
try {
|
||||
// 기본 설정 저장
|
||||
if ($_POST['action'] == 'save_basic_settings') {
|
||||
$basic_settings = [
|
||||
'consultation_duration' => (int) ($_POST['consultation_duration'] ?? 60),
|
||||
'max_persons_per_slot' => (int) ($_POST['max_persons_per_slot'] ?? 2),
|
||||
'consultation_fee' => (int) ($_POST['consultation_fee'] ?? 50000),
|
||||
'account_info' => trim($_POST['account_info'] ?? ''),
|
||||
'max_advance_days' => (int) ($_POST['max_advance_days'] ?? 30),
|
||||
'min_advance_hours' => (int) ($_POST['min_advance_hours'] ?? 24),
|
||||
'cancel_deadline_hours' => (int) ($_POST['cancel_deadline_hours'] ?? 24)
|
||||
];
|
||||
|
||||
// 유효성 검증 (생략)
|
||||
|
||||
foreach ($basic_settings as $key => $value) {
|
||||
consultant_set_config($key, $value);
|
||||
}
|
||||
alert('기본 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=basic');
|
||||
}
|
||||
|
||||
// 요일별 설정 저장
|
||||
if ($_POST['action'] == 'save_weekly_settings') {
|
||||
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
foreach ($days as $day) {
|
||||
consultant_set_config($day . '_enabled', $_POST[$day . '_enabled'] ?? '0');
|
||||
consultant_set_config($day . '_start', $_POST[$day . '_start'] ?? '09:00');
|
||||
consultant_set_config($day . '_end', $_POST[$day . '_end'] ?? '18:00');
|
||||
consultant_set_config($day . '_lunch_start', $_POST[$day . '_lunch_start'] ?? '12:00');
|
||||
consultant_set_config($day . '_lunch_end', $_POST[$day . '_lunch_end'] ?? '13:00');
|
||||
}
|
||||
alert('요일별 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=weekly');
|
||||
}
|
||||
|
||||
// 알림 설정 저장
|
||||
if ($_POST['action'] == 'save_notification_settings') {
|
||||
consultant_set_config('notification_enabled', $_POST['notification_enabled'] ?? '0');
|
||||
alert('알림 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=notification');
|
||||
}
|
||||
|
||||
// 💡 [추가] 고급 설정 저장 처리
|
||||
if ($_POST['action'] == 'save_advanced_settings') {
|
||||
$config_values = $_POST['config_value'] ?? [];
|
||||
$config_descs = $_POST['config_desc'] ?? [];
|
||||
|
||||
foreach ($config_values as $key => $value) {
|
||||
$sql = "UPDATE consultant_config
|
||||
SET config_value = '" . sql_real_escape_string($value) . "',
|
||||
config_desc = '" . sql_real_escape_string($config_descs[$key] ?? '') . "'
|
||||
WHERE config_key = '" . sql_real_escape_string($key) . "'";
|
||||
sql_query($sql);
|
||||
}
|
||||
alert('고급 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=advanced');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert('설정 저장 중 오류가 발생했습니다: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- 데이터 조회 ---
|
||||
// 기본 설정
|
||||
$consultation_duration = consultant_get_config('consultation_duration', '60');
|
||||
$max_persons_per_slot = consultant_get_config('max_persons_per_slot', '2');
|
||||
$consultation_fee = consultant_get_config('consultation_fee', '50000');
|
||||
$account_info = consultant_get_config('account_info', '국민은행 123-456-789 (주)상담센터');
|
||||
$notification_enabled = consultant_get_config('notification_enabled', '1');
|
||||
$max_advance_days = consultant_get_config('max_advance_days', '30');
|
||||
$min_advance_hours = consultant_get_config('min_advance_hours', '24');
|
||||
$cancel_deadline_hours = consultant_get_config('cancel_deadline_hours', '24');
|
||||
|
||||
// 요일별 설정
|
||||
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
$day_names = ['월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
|
||||
$weekly_settings = [];
|
||||
foreach ($days as $i => $day) {
|
||||
$weekly_settings[$day] = [
|
||||
'name' => $day_names[$i],
|
||||
'enabled' => consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1'),
|
||||
'start' => consultant_get_config($day . '_start', '09:00'),
|
||||
'end' => consultant_get_config($day . '_end', '18:00'),
|
||||
'lunch_start' => consultant_get_config($day . '_lunch_start', '12:00'),
|
||||
'lunch_end' => consultant_get_config($day . '_lunch_end', '13:00')
|
||||
];
|
||||
}
|
||||
|
||||
// 💡 [추가] 고급 설정 데이터 조회
|
||||
$advanced_configs = [];
|
||||
$result = sql_query("SELECT * FROM consultant_config ORDER BY id");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$advanced_configs[] = $row;
|
||||
}
|
||||
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
<div>
|
||||
<a href="dashboard.php" class="header-btn">📊 대시보드</a>
|
||||
<a href="schedule_generate.php" class="header-btn primary">📅 빠른 스케줄 관리</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="settings-tabs">
|
||||
<a href="?tab=basic" class="tab-button <?php echo $current_tab == 'basic' ? 'active' : ''; ?>">⚙️ 기본 설정</a>
|
||||
<a href="?tab=weekly" class="tab-button <?php echo $current_tab == 'weekly' ? 'active' : ''; ?>">📅 요일별 운영시간</a>
|
||||
<a href="?tab=advanced" class="tab-button <?php echo $current_tab == 'advanced' ? 'active' : ''; ?>">🛠️ 고급 설정</a>
|
||||
<a href="?tab=notification" class="tab-button <?php echo $current_tab == 'notification' ? 'active' : ''; ?>">🔔 알림 설정</a>
|
||||
</div>
|
||||
|
||||
<!-- 기본 설정 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'basic' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>기본 설정:</strong> 1회 상담시간, 최대인원, 상담비 등 기본적인 상담 조건을 설정합니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_basic_settings">
|
||||
|
||||
<div class="section-title">⏰ 상담 기본 조건</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="consultation_duration">1회 상담 시간 (분)</label>
|
||||
<select id="consultation_duration" name="consultation_duration">
|
||||
<?php
|
||||
for ($i = 15; $i <= 480; $i += 15) {
|
||||
$selected = ($consultation_duration == $i) ? 'selected' : '';
|
||||
echo "<option value=\"{$i}\" {$selected}>{$i}분</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<small>15분~480분 사이로 설정 가능합니다. (15분 단위)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="max_persons_per_slot">1회 상담 최대 인원 (명)</label>
|
||||
<input type="number" id="max_persons_per_slot" name="max_persons_per_slot" value="<?php echo htmlspecialchars($max_persons_per_slot); ?>" min="1" max="50" placeholder="2">
|
||||
<small>1명~50명 사이로 설정 가능합니다.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">💰 결제 정보</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="consultation_fee">상담 비용 (원)</label>
|
||||
<input type="number" id="consultation_fee" name="consultation_fee" value="<?php echo htmlspecialchars($consultation_fee); ?>" min="0" step="1000" placeholder="50000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_info">입금 계좌 정보</label>
|
||||
<textarea id="account_info" name="account_info" placeholder="예: 국민은행 123-456-789 (주)상담센터"><?php echo htmlspecialchars($account_info); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="section-title">📅 예약 제한 설정</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="max_advance_days">최대 예약 가능 일수</label>
|
||||
<input type="number" id="max_advance_days" name="max_advance_days" value="<?php echo htmlspecialchars($max_advance_days); ?>" min="1" max="365">
|
||||
<small>오늘부터 몇 일 후까지 예약 가능한지 설정</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min_advance_hours">최소 예약 시간 (시간)</label>
|
||||
<input type="number" id="min_advance_hours" name="min_advance_hours" value="<?php echo htmlspecialchars($min_advance_hours); ?>" min="1" max="168">
|
||||
<small>최소 몇 시간 전에 예약해야 하는지 설정</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cancel_deadline_hours">예약 취소 마감 (시간)</label>
|
||||
<input type="number" id="cancel_deadline_hours" name="cancel_deadline_hours" value="<?php echo htmlspecialchars($cancel_deadline_hours); ?>" min="1" max="168">
|
||||
<small>상담 시작 몇 시간 전까지 고객이 직접 취소할 수 있는지 설정</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">기본 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 요일별 운영시간 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'weekly' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>요일별 운영시간:</strong> 각 요일의 상담 운영 여부와 시간을 설정합니다. '운영'을 선택해야 해당 요일의 스케줄이 생성됩니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_weekly_settings">
|
||||
|
||||
<?php foreach ($weekly_settings as $day => $setting): ?>
|
||||
<div class="day-setting">
|
||||
<div class="day-header">
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="hidden" name="<?php echo $day; ?>_enabled" value="0">
|
||||
<input type="checkbox" id="<?php echo $day; ?>_enabled" name="<?php echo $day; ?>_enabled" value="1" <?php echo $setting['enabled'] == '1' ? 'checked' : ''; ?> onchange="toggleDayTimes('<?php echo $day; ?>')">
|
||||
<label for="<?php echo $day; ?>_enabled" class="day-name"><?php echo $setting['name']; ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="day-times" id="<?php echo $day; ?>_times">
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_start">업무 시작</label>
|
||||
<input type="time" id="<?php echo $day; ?>_start" name="<?php echo $day; ?>_start" value="<?php echo htmlspecialchars($setting['start']); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_end">업무 종료</label>
|
||||
<input type="time" id="<?php echo $day; ?>_end" name="<?php echo $day; ?>_end" value="<?php echo htmlspecialchars($setting['end']); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_lunch_start">점심 시작</label>
|
||||
<input type="time" id="<?php echo $day; ?>_lunch_start" name="<?php echo $day; ?>_lunch_start" value="<?php echo htmlspecialchars($setting['lunch_start']); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_lunch_end">점심 종료</label>
|
||||
<input type="time" id="<?php echo $day; ?>_lunch_end" name="<?php echo $day; ?>_lunch_end" value="<?php echo htmlspecialchars($setting['lunch_end']); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">요일별 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [추가] 고급 설정 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'advanced' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>고급 설정:</strong> 시스템의 모든 설정값을 직접 관리합니다. <strong>'Key'는 시스템에서 사용하는 고유값이므로 변경할 수 없습니다.</strong><br>
|
||||
'Value'는 실제 적용되는 값이며, 'Description'은 관리자가 참고하기 위한 설명입니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_advanced_settings">
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>고급 설정 목록</caption>
|
||||
<colgroup>
|
||||
<col style="width: 25%;">
|
||||
<col style="width: 40%;">
|
||||
<col style="width: 35%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Key (변경불가)</th>
|
||||
<th scope="col">Value (설정값)</th>
|
||||
<th scope="col">Description (설명)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($advanced_configs as $config_item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="config_value_<?php echo $config_item['config_key']; ?>">
|
||||
<code style="font-size: 13px; font-weight: bold;"><?php echo htmlspecialchars($config_item['config_key']); ?></code>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (strlen($config_item['config_value']) > 50 || strpos($config_item['config_value'], "\n") !== false): ?>
|
||||
<textarea class="form-control" id="config_value_<?php echo $config_item['config_key']; ?>" name="config_value[<?php echo $config_item['config_key']; ?>]" rows="2"><?php echo htmlspecialchars($config_item['config_value']); ?></textarea>
|
||||
<?php else: ?>
|
||||
<input type="text" class="form-control" id="config_value_<?php echo $config_item['config_key']; ?>" name="config_value[<?php echo $config_item['config_key']; ?>]" value="<?php echo htmlspecialchars($config_item['config_value']); ?>">
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" name="config_desc[<?php echo $config_item['config_key']; ?>]" value="<?php echo htmlspecialchars($config_item['config_desc']); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">고급 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 알림 설정 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'notification' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>알림 설정:</strong> 예약 관련 알림 기능을 설정합니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_notification_settings">
|
||||
<div class="section-title">🔔 알림 기능</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox" id="notification_enabled" name="notification_enabled" value="1" <?php echo $notification_enabled == '1' ? 'checked' : ''; ?>>
|
||||
<label for="notification_enabled">알림 기능 사용</label>
|
||||
</div>
|
||||
<small>예약 확정, 취소 등의 상황에서 고객에게 알림을 발송합니다.</small>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">알림 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [복구] 깨진 화면을 복구하기 위해 CSS와 JS를 파일 내에 다시 포함합니다. -->
|
||||
<style>
|
||||
.settings-container { max-width: 1000px; margin: 0 auto; padding: 20px; }
|
||||
.settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.settings-header div { display: flex; gap: 8px; }
|
||||
.settings-tabs { display: flex; border-bottom: 2px solid #ddd; margin-bottom: 30px; }
|
||||
.tab-button { padding: 16px 28px; border: none; background: none; cursor: pointer; font-size: 16px; font-weight: 600; color: #666; text-decoration: none; border-bottom: 3px solid transparent; transition: all 0.3s; }
|
||||
.tab-button.active { color: #007bff; border-bottom-color: #007bff; }
|
||||
.tab-button:hover { color: #007bff; background: #fff; }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.settings-form { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #333; }
|
||||
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 12px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; min-height: 44px; line-height: 1.4; }
|
||||
.form-group textarea { height: auto; resize: vertical; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.btn { padding: 16px 32px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 16px; text-decoration: none; display: inline-block; transition: all 0.2s; text-align: center; min-height: 50px; line-height: 1.4; box-sizing: border-box; vertical-align: middle; }
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-primary:hover { background: #0056b3; }
|
||||
.alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
|
||||
.alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; }
|
||||
.section-title { font-size: 18px; font-weight: bold; margin: 30px 0 15px 0; padding-bottom: 8px; border-bottom: 2px solid #007bff; color: #333; }
|
||||
.section-title:first-child { margin-top: 0; }
|
||||
.day-setting { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
|
||||
.day-header { display: flex; align-items: center; margin-bottom: 15px; gap: 15px; }
|
||||
.day-name { font-weight: bold; font-size: 16px; color: #333; min-width: 80px; }
|
||||
.day-times { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 15px; }
|
||||
.day-times.disabled { opacity: 0.5; pointer-events: none; }
|
||||
.checkbox-wrapper { display: flex; align-items: center; gap: 8px; }
|
||||
.checkbox-wrapper input[type="checkbox"] { width: 18px; height: 18px; margin: 0; }
|
||||
.form-group small { display: block; margin-top: 5px; font-size: 12px; color: #666; line-height: 1.3; }
|
||||
.header-btn { padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 14px; text-decoration: none; display: inline-block; transition: all 0.2s; text-align: center; min-height: auto; line-height: 1.2; box-sizing: border-box; background: #fff; color: #333; }
|
||||
.header-btn:hover { background: #fff; border-color: #adb5bd; color: #333; }
|
||||
.header-btn.primary { background: #e3f2fd; border-color: #90caf9; color: #1976d2; }
|
||||
.header-btn.primary:hover { background: #bbdefb; border-color: #64b5f6; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleDayTimes(day) {
|
||||
const checkbox = document.getElementById(day + '_enabled');
|
||||
const timesDiv = document.getElementById(day + '_times');
|
||||
if (checkbox?.checked) {
|
||||
timesDiv.classList.remove('disabled');
|
||||
} else {
|
||||
timesDiv?.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
days.forEach(function (day) {
|
||||
toggleDayTimes(day);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 통계 분석
|
||||
*/
|
||||
$sub_menu = '850400';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '통계 분석';
|
||||
|
||||
// 기간 설정
|
||||
$start_date = $_GET['start_date'] ?? date('Y-m-01'); // 이번 달 첫날
|
||||
$end_date = $_GET['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 consultant_reservations
|
||||
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND is_deleted = 0";
|
||||
$total_stats = sql_fetch($sql);
|
||||
|
||||
// 일별 통계
|
||||
$sql = "SELECT
|
||||
reservation_date,
|
||||
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 revenue
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_date
|
||||
ORDER BY reservation_date DESC";
|
||||
$daily_stats = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$daily_stats[] = $row;
|
||||
}
|
||||
|
||||
// 시간대별 통계
|
||||
$sql = "SELECT
|
||||
reservation_time,
|
||||
COUNT(*) as count
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_time
|
||||
ORDER BY reservation_time";
|
||||
$time_stats = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$time_stats[] = $row;
|
||||
}
|
||||
|
||||
// 상태별 통계
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.statistics-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-card.total .stat-number {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.stat-card.pending .stat-number {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.stat-card.confirmed .stat-number {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.stat-card.completed .stat-number {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.stat-card.cancelled .stat-number {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.stat-card.revenue .stat-number {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.table-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
height: 200px;
|
||||
gap: 10px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.bar {
|
||||
background: #007bff;
|
||||
border-radius: 4px 4px 0 0;
|
||||
min-width: 40px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
transform: rotate(-45deg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="statistics-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<!-- 기간 필터 -->
|
||||
<form method="get" class="filter-form">
|
||||
<label>
|
||||
시작일:
|
||||
<input type="date" name="start_date" value="<?php echo $start_date; ?>" class="form-control">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
종료일:
|
||||
<input type="date" name="end_date" value="<?php echo $end_date; ?>" class="form-control">
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary">조회</button>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</form>
|
||||
|
||||
<!-- 전체 통계 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card total">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['total']); ?></div>
|
||||
<div class="stat-label">전체 예약</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card pending">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['pending']); ?></div>
|
||||
<div class="stat-label">입금대기</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card confirmed">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['confirmed']); ?></div>
|
||||
<div class="stat-label">예약확정</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card completed">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['completed']); ?></div>
|
||||
<div class="stat-label">상담완료</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card cancelled">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['cancelled']); ?></div>
|
||||
<div class="stat-label">예약취소</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card revenue">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['total_revenue']); ?>원</div>
|
||||
<div class="stat-label">총 매출</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 시간대별 예약 현황 -->
|
||||
<?php if (!empty($time_stats)): ?>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">시간대별 예약 현황</div>
|
||||
<div class="bar-chart">
|
||||
<?php
|
||||
$max_count = max(array_column($time_stats, 'count'));
|
||||
foreach ($time_stats as $stat):
|
||||
$height = $max_count > 0 ? ($stat['count'] / $max_count) * 150 : 0;
|
||||
?>
|
||||
<div class="bar" style="height: <?php echo $height; ?>px;">
|
||||
<div class="bar-value"><?php echo $stat['count']; ?></div>
|
||||
<div class="bar-label"><?php echo substr($stat['reservation_time'], 0, 5); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 일별 상세 통계 -->
|
||||
<div class="table-container">
|
||||
<div class="table-header">일별 예약 현황</div>
|
||||
<div class="table-content">
|
||||
<?php if (!empty($daily_stats)): ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>날짜</th>
|
||||
<th>전체</th>
|
||||
<th>입금대기</th>
|
||||
<th>예약확정</th>
|
||||
<th>상담완료</th>
|
||||
<th>예약취소</th>
|
||||
<th>매출</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($daily_stats as $stat): ?>
|
||||
<tr>
|
||||
<td><?php echo $stat['reservation_date']; ?></td>
|
||||
<td><?php echo number_format($stat['total']); ?></td>
|
||||
<td><?php echo number_format($stat['pending']); ?></td>
|
||||
<td><?php echo number_format($stat['confirmed']); ?></td>
|
||||
<td><?php echo number_format($stat['completed']); ?></td>
|
||||
<td><?php echo number_format($stat['cancelled']); ?></td>
|
||||
<td><?php echo number_format($stat['revenue']); ?>원</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="no-data">
|
||||
선택한 기간에 예약 데이터가 없습니다.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 차트 애니메이션
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const bars = document.querySelectorAll('.bar');
|
||||
bars.forEach((bar, index) => {
|
||||
setTimeout(() => {
|
||||
bar.style.opacity = '0';
|
||||
bar.style.transform = 'scaleY(0)';
|
||||
bar.style.transformOrigin = 'bottom';
|
||||
|
||||
setTimeout(() => {
|
||||
bar.style.transition = 'all 0.5s ease';
|
||||
bar.style.opacity = '1';
|
||||
bar.style.transform = 'scaleY(1)';
|
||||
}, 100);
|
||||
}, index * 100);
|
||||
});
|
||||
});
|
||||
|
||||
// 기간 설정 단축키
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const startDateInput = document.querySelector('input[name="start_date"]');
|
||||
const endDateInput = document.querySelector('input[name="end_date"]');
|
||||
|
||||
// 오늘 날짜
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
|
||||
// 이번 주 시작일 (월요일)
|
||||
const thisWeekStart = new Date(today);
|
||||
thisWeekStart.setDate(today.getDate() - today.getDay() + 1);
|
||||
const thisWeekStartStr = thisWeekStart.toISOString().split('T')[0];
|
||||
|
||||
// 이번 달 시작일
|
||||
const thisMonthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const thisMonthStartStr = thisMonthStart.toISOString().split('T')[0];
|
||||
|
||||
// 단축키 버튼들 추가
|
||||
const filterForm = document.querySelector('.filter-form');
|
||||
const shortcutButtons = document.createElement('div');
|
||||
shortcutButtons.innerHTML = `
|
||||
<button type="button" onclick="setDateRange('${todayStr}', '${todayStr}')" class="btn btn-secondary">오늘</button>
|
||||
<button type="button" onclick="setDateRange('${thisWeekStartStr}', '${todayStr}')" class="btn btn-secondary">이번주</button>
|
||||
<button type="button" onclick="setDateRange('${thisMonthStartStr}', '${todayStr}')" class="btn btn-secondary">이번달</button>
|
||||
`;
|
||||
filterForm.appendChild(shortcutButtons);
|
||||
|
||||
window.setDateRange = function (start, end) {
|
||||
startDateInput.value = start;
|
||||
endDateInput.value = end;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
/**
|
||||
* 알림 템플릿 관리
|
||||
*/
|
||||
$sub_menu = '850620';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '알림 템플릿 관리';
|
||||
|
||||
// 템플릿 저장 처리
|
||||
if ($_POST['action'] == 'save_template') {
|
||||
$template_key = $_POST['template_key'];
|
||||
$template_subject = $_POST['template_subject'];
|
||||
$template_type = $_POST['template_type'] ?? 'email'; // 💡 [추가] 템플릿 타입
|
||||
$template_content = $_POST['template_content'];
|
||||
$template_name = $_POST['template_name']; // 💡 [추가] 템플릿 이름을 폼에서 받아옵니다.
|
||||
|
||||
if ($template_key && $template_subject && $template_content) {
|
||||
// 템플릿 저장/업데이트
|
||||
// 💡 [수정] 타입에 따라 테이블 분기
|
||||
$table_name = ($template_type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
|
||||
|
||||
$sql = "INSERT INTO {$table_name}
|
||||
(template_key, template_type, template_name, template_subject, template_content, updated_at)
|
||||
VALUES (
|
||||
'" . sql_real_escape_string($template_key) . "',
|
||||
'" . sql_real_escape_string($template_type) . "',
|
||||
'" . sql_real_escape_string($template_name) . "',
|
||||
'" . sql_real_escape_string($template_subject) . "',
|
||||
'" . sql_real_escape_string($template_content) . "',
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
template_subject = '" . sql_real_escape_string($template_subject) . "',
|
||||
template_type = '" . sql_real_escape_string($template_type) . "',
|
||||
template_content = '" . sql_real_escape_string($template_content) . "',
|
||||
updated_at = NOW()";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
alert('템플릿이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?type=' . $template_type . '&template=' . $template_key);
|
||||
} else {
|
||||
alert('템플릿 저장에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 [추가] 현재 탭 확인
|
||||
$current_type = $_GET['type'] ?? 'email';
|
||||
|
||||
// 기본 템플릿 정의
|
||||
$default_templates = [
|
||||
'consultant_reservation_customer' => [
|
||||
'name' => '고객 예약 신청 확인',
|
||||
'subject' => '[상담예약] 예약 신청이 접수되었습니다',
|
||||
'content' => "안녕하세요 {customer_name}님,\n\n상담 예약 신청이 정상적으로 접수되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n- 상담비: {payment_amount}원\n\n입금 계좌: {account_info}\n\n입금 확인 후 예약이 확정됩니다.\n\n감사합니다."
|
||||
],
|
||||
'consultant_confirmed_customer' => [
|
||||
'name' => '고객 예약 확정 알림',
|
||||
'subject' => '[상담예약] 예약이 확정되었습니다',
|
||||
'content' => "안녕하세요 {customer_name}님,\n\n입금이 확인되어 예약이 확정되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n상담 당일 시간에 맞춰 방문해주시기 바랍니다.\n\n감사합니다."
|
||||
],
|
||||
'consultant_cancelled_customer' => [
|
||||
'name' => '고객 예약 취소 알림',
|
||||
'subject' => '[상담예약] 예약이 취소되었습니다',
|
||||
'content' => "안녕하세요 {customer_name}님,\n\n예약이 취소되었습니다.\n\n취소된 예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n취소 사유: {cancel_reason}\n\n문의사항이 있으시면 연락주시기 바랍니다.\n\n감사합니다."
|
||||
]
|
||||
];
|
||||
|
||||
// 현재 템플릿 조회
|
||||
$templates = [];
|
||||
// 💡 [수정] 현재 탭의 타입에 맞는 템플릿만 조회합니다.
|
||||
if (is_consultant_installed()) {
|
||||
// 💡 [수정] 타입에 따라 테이블 분기
|
||||
$table_name = ($current_type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
|
||||
|
||||
// 테이블 존재 여부 확인 (설치 초기 단계 고려)
|
||||
$table_check = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
|
||||
if (sql_num_rows($table_check) > 0) {
|
||||
$sql = "SELECT * FROM {$table_name} WHERE template_type = '".sql_real_escape_string($current_type)."' ORDER BY template_key";
|
||||
$result = sql_query($sql, false);
|
||||
if ($result) {
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$templates[$row['template_key']] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 템플릿과 병합
|
||||
foreach ($default_templates as $key => $default) {
|
||||
if (!isset($templates[$key])) {
|
||||
$templates[$key] = [
|
||||
'template_key' => $key,
|
||||
'template_name' => $default['name'],
|
||||
'template_subject' => $default['subject'],
|
||||
'template_content' => $default['content']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$current_template_key = $_GET['template'] ?? array_key_first($templates);
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.templates-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.template-nav {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.template-tabs {
|
||||
display: flex; border-bottom: 2px solid #ddd; margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.template-tab {
|
||||
padding: 16px 28px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-tab.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.template-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.variables-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.variables-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.variables-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.variable-item {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.template-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.variables-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="templates-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>안내:</strong> 예약 관련 자동 발송 <?php echo ($current_type == 'sms') ? '문자' : '이메일'; ?>의 템플릿을 관리합니다.
|
||||
중괄호 {} 안의 변수들은 실제 데이터로 자동 치환됩니다.
|
||||
</div>
|
||||
|
||||
<!-- 템플릿 탭 -->
|
||||
<div class="template-nav">
|
||||
<div class="template-tabs">
|
||||
<a href="?type=email" class="template-tab <?php echo $current_type == 'email' ? 'active' : ''; ?>">📧 메일 템플릿</a>
|
||||
<a href="?type=sms" class="template-tab <?php echo $current_type == 'sms' ? 'active' : ''; ?>">📱 문자 템플릿</a>
|
||||
</div>
|
||||
|
||||
<?php if(!empty($templates)): ?>
|
||||
<div class="template-tabs" style="border-bottom:none; margin-bottom: 10px;">
|
||||
<?php foreach ($templates as $key => $template): ?>
|
||||
<a href="?type=<?php echo $current_type; ?>&template=<?php echo $key; ?>"
|
||||
class="template-tab <?php echo $current_template_key == $key ? 'active' : ''; ?>" style="font-size:14px; padding: 8px 16px;">
|
||||
<?php echo htmlspecialchars($template['template_name']); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 사용 가능한 변수 안내 -->
|
||||
<div class="variables-info">
|
||||
<div class="variables-title">📝 사용 가능한 변수</div>
|
||||
<div class="variables-list">
|
||||
<div class="variable-item">{customer_name} - 고객명</div>
|
||||
<div class="variable-item">{customer_phone} - 고객 연락처</div>
|
||||
<div class="variable-item">{customer_email} - 고객 이메일</div>
|
||||
<div class="variable-item">{reservation_date} - 예약 날짜</div>
|
||||
<div class="variable-item">{reservation_time} - 예약 시간</div>
|
||||
<div class="variable-item">{payment_amount} - 상담 비용</div>
|
||||
<div class="variable-item">{account_info} - 입금 계좌</div>
|
||||
<div class="variable-item">{cancel_reason} - 취소 사유</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 템플릿 편집 폼 -->
|
||||
<?php if (isset($templates[$current_template_key])): ?>
|
||||
<?php $template = $templates[$current_template_key]; ?>
|
||||
<form method="post" class="template-form">
|
||||
<input type="hidden" name="action" value="save_template">
|
||||
<input type="hidden" name="template_key" value="<?php echo $current_template_key; ?>">
|
||||
<input type="hidden" name="template_name" value="<?php echo htmlspecialchars($template['template_name']); ?>">
|
||||
<input type="hidden" name="template_type" value="<?php echo $current_type; ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_subject"><?php echo ($current_type == 'sms') ? '문자 제목 (LMS용)' : '이메일 제목'; ?></label>
|
||||
<input type="text" id="template_subject" name="template_subject"
|
||||
value="<?php echo htmlspecialchars($template['template_subject']); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_content"><?php echo ($current_type == 'sms') ? '문자 내용' : '이메일 내용'; ?></label>
|
||||
<textarea id="template_content" name="template_content" required><?php echo htmlspecialchars($template['template_content']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<!-- 💡 [개선] 미리보기 버튼을 JS로 생성하는 대신 HTML에 직접 추가하여 안정성을 높이고, 저장 버튼과 나란히 배치합니다. -->
|
||||
<button type="button" onclick="previewTemplate()" class="btn btn-secondary" style="margin-right: 10px;">미리보기</button>
|
||||
<button type="submit" class="btn btn-primary">템플릿 저장</button>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 변수 삽입 도우미
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const textarea = document.getElementById('template_content');
|
||||
const variableItems = document.querySelectorAll('.variable-item');
|
||||
|
||||
variableItems.forEach(item => {
|
||||
item.style.cursor = 'pointer';
|
||||
item.title = '클릭하여 템플릿에 삽입';
|
||||
|
||||
item.addEventListener('click', function() {
|
||||
const variable = this.textContent.split(' - ')[0];
|
||||
const cursorPos = textarea.selectionStart;
|
||||
const textBefore = textarea.value.substring(0, cursorPos);
|
||||
const textAfter = textarea.value.substring(cursorPos);
|
||||
|
||||
textarea.value = textBefore + variable + textAfter;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(cursorPos + variable.length, cursorPos + variable.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 템플릿 미리보기
|
||||
function previewTemplate() {
|
||||
const subject = document.getElementById('template_subject').value;
|
||||
const content = document.getElementById('template_content').value;
|
||||
|
||||
// 샘플 데이터로 치환
|
||||
const sampleData = {
|
||||
'{customer_name}': '홍길동',
|
||||
'{customer_phone}': '010-1234-5678',
|
||||
'{customer_email}': 'hong@example.com',
|
||||
'{reservation_date}': '2024-12-15',
|
||||
'{reservation_time}': '14:00',
|
||||
'{payment_amount}': '50,000',
|
||||
'{account_info}': '국민은행 123-456-789 (주)상담센터',
|
||||
'{cancel_reason}': '개인 사정'
|
||||
};
|
||||
|
||||
let previewSubject = subject;
|
||||
let previewContent = content;
|
||||
|
||||
for (const [variable, value] of Object.entries(sampleData)) {
|
||||
previewSubject = previewSubject.replace(new RegExp(variable.replace(/[{}]/g, '\\\\$&'), 'g'), value);
|
||||
previewContent = previewContent.replace(new RegExp(variable.replace(/[{}]/g, '\\\\$&'), 'g'), value);
|
||||
}
|
||||
|
||||
// 💡 [개선] 실제 이메일처럼 보이도록 nl2br 처리 및 UI 개선
|
||||
const previewHtmlContent = previewContent.replace(/\n/g, '<br>');
|
||||
|
||||
// 💡 [개선] document.write() 대신 DOM 조작을 사용하여 안정성을 높입니다.
|
||||
const previewWindow = window.open('', '_blank', 'width=800,height=600');
|
||||
const previewDoc = previewWindow.document;
|
||||
|
||||
previewDoc.open();
|
||||
previewDoc.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>미리보기</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f6f8fa; }
|
||||
.preview-container { max-width: 800px; margin: 20px auto; background-color: #fff; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 1px 5px rgba(0,0,0,0.1); }
|
||||
.preview-header { padding: 20px; border-bottom: 1px solid #eee; }
|
||||
.preview-header h2 { margin: 0; font-size: 20px; color: #333; }
|
||||
.preview-meta { padding: 15px 20px; background-color: #fdfdfd; border-bottom: 1px solid #eee; font-size: 14px; }
|
||||
.meta-item { display: flex; margin-bottom: 8px; }
|
||||
.meta-label { font-weight: bold; color: #555; width: 80px; }
|
||||
.meta-value { color: #333; }
|
||||
.preview-body { padding: 30px 20px; line-height: 1.7; color: #333; font-size: 15px; }
|
||||
</style>
|
||||
</head>
|
||||
<bo`+`dy>
|
||||
<div class='preview-container'>
|
||||
<div class="preview-header"><h2>미리보기</h2></div>
|
||||
<div class="preview-meta">
|
||||
<div class="meta-item"><span class="meta-label">보내는사람:</span><span class="meta-value">관리자 <admin@example.com></span></div>
|
||||
<div class="meta-item"><span class="meta-label">받는사람:</span><span class="meta-value">홍길동 <hong@example.com></span></div>
|
||||
<div class="meta-item"><span class="meta-label">제 목:</span><span class="meta-value">${previewSubject}</span></div>
|
||||
</div>
|
||||
<div class="preview-body">${previewHtmlContent}</div>
|
||||
</div>
|
||||
</bo`+`dy>
|
||||
</html>
|
||||
`);
|
||||
previewWindow.document.close();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 테스트 페이지
|
||||
*/
|
||||
|
||||
// 오류 표시 활성화
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
echo "<h1>상담 예약 시스템 테스트</h1>";
|
||||
|
||||
// 1. 그누보드 기본 파일 로드 테스트
|
||||
echo "<h3>1. 그누보드 기본 파일 로드 테스트</h3>";
|
||||
try {
|
||||
include_once('./_common_con.php');
|
||||
echo "✅ common.php 로드 성공<br>";
|
||||
echo "✅ 데이터베이스 연결: " . (isset($connect_db) ? "성공" : "실패") . "<br>";
|
||||
echo "✅ 관리자 권한: " . ($is_admin ? "있음" : "없음") . "<br>";
|
||||
echo "✅ G5_PATH: " . (defined('G5_PATH') ? G5_PATH : "정의되지 않음") . "<br>";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ common.php 로드 실패: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 2. 함수 존재 확인
|
||||
echo "<h3>2. 필수 함수 존재 확인</h3>";
|
||||
$functions = ['sql_query', 'sql_fetch', 'sql_real_escape_string', 'alert'];
|
||||
foreach ($functions as $func) {
|
||||
echo (function_exists($func) ? "✅" : "❌") . " {$func}<br>";
|
||||
}
|
||||
|
||||
// 3. 상수 확인
|
||||
echo "<h3>3. 필수 상수 확인</h3>";
|
||||
$constants = ['G5_PATH', 'G5_ADMIN_PATH', 'G5_DATA_PATH'];
|
||||
foreach ($constants as $const) {
|
||||
echo (defined($const) ? "✅" : "❌") . " {$const}: " . (defined($const) ? constant($const) : "정의되지 않음") . "<br>";
|
||||
}
|
||||
|
||||
// 4. 데이터베이스 연결 테스트
|
||||
echo "<h3>4. 데이터베이스 연결 테스트</h3>";
|
||||
try {
|
||||
$sql = "SELECT 1 as test";
|
||||
$result = sql_query($sql);
|
||||
if ($result) {
|
||||
echo "✅ 데이터베이스 쿼리 성공<br>";
|
||||
} else {
|
||||
echo "❌ 데이터베이스 쿼리 실패<br>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "❌ 데이터베이스 오류: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 5. 테이블 존재 확인
|
||||
echo "<h3>5. 상담 시스템 테이블 확인</h3>";
|
||||
$tables = ['consultant_config', 'consultant_schedule', 'consultant_reservations'];
|
||||
foreach ($tables as $table) {
|
||||
$sql = "SHOW TABLES LIKE '{$table}'";
|
||||
$result = sql_query($sql, false);
|
||||
$exists = $result && sql_num_rows($result) > 0;
|
||||
echo ($exists ? "✅" : "❌") . " {$table}<br>";
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
echo "<p><a href='install_simple.php'>간단 설치 페이지로 이동</a></p>";
|
||||
echo "<p><a href='../install.php'>원본 설치 페이지로 이동</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 설치 테스트
|
||||
*/
|
||||
|
||||
// 오류 표시 활성화
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
echo "<h1>상담 예약 시스템 설치 테스트</h1>";
|
||||
|
||||
// 1. 그누보드 기본 파일 로드 테스트
|
||||
echo "<h3>1. 그누보드 기본 파일 로드 테스트</h3>";
|
||||
try {
|
||||
include_once('../../common.php');
|
||||
echo "✅ common.php 로드 성공<br>";
|
||||
echo "✅ 데이터베이스 연결: " . (isset($connect_db) ? "성공" : "실패") . "<br>";
|
||||
echo "✅ 관리자 권한: " . ($is_admin ? "있음" : "없음") . "<br>";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ common.php 로드 실패: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 2. _common.php 로드 테스트
|
||||
echo "<h3>2. _common.php 로드 테스트</h3>";
|
||||
try {
|
||||
include_once('./_common.php');
|
||||
echo "✅ _common.php 로드 성공<br>";
|
||||
echo "✅ 상담 시스템 버전: " . (defined('G5_CONSULTANT_VERSION') ? G5_CONSULTANT_VERSION : "정의되지 않음") . "<br>";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ _common.php 로드 실패: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 3. 함수 존재 확인
|
||||
echo "<h3>3. 필수 함수 존재 확인</h3>";
|
||||
$functions = ['sql_query', 'sql_fetch', 'sql_real_escape_string', 'alert'];
|
||||
foreach ($functions as $func) {
|
||||
echo (function_exists($func) ? "✅" : "❌") . " {$func}<br>";
|
||||
}
|
||||
|
||||
// 4. 상수 확인
|
||||
echo "<h3>4. 필수 상수 확인</h3>";
|
||||
$constants = ['G5_PATH', 'G5_ADMIN_PATH', 'G5_DATA_PATH'];
|
||||
foreach ($constants as $const) {
|
||||
echo (defined($const) ? "✅" : "❌") . " {$const}: " . (defined($const) ? constant($const) : "정의되지 않음") . "<br>";
|
||||
}
|
||||
|
||||
// 5. 데이터베이스 연결 테스트
|
||||
echo "<h3>5. 데이터베이스 연결 테스트</h3>";
|
||||
try {
|
||||
$sql = "SELECT 1 as test";
|
||||
$result = sql_query($sql);
|
||||
if ($result) {
|
||||
echo "✅ 데이터베이스 쿼리 성공<br>";
|
||||
} else {
|
||||
echo "❌ 데이터베이스 쿼리 실패<br>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "❌ 데이터베이스 오류: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
echo "<p><a href='../install.php'>설치 페이지로 이동</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* 스케줄 생성 테스트 페이지
|
||||
*/
|
||||
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '스케줄 생성 테스트';
|
||||
|
||||
// 테스트 실행
|
||||
if ($_POST['action'] == 'test_generation') {
|
||||
$year = (int) ($_POST['year'] ?? date('Y'));
|
||||
$month = (int) ($_POST['month'] ?? date('n'));
|
||||
|
||||
try {
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
$generator = new ScheduleGenerator();
|
||||
|
||||
echo "<h3>테스트 결과</h3>";
|
||||
echo "<p><strong>대상:</strong> {$year}년 {$month}월</p>";
|
||||
|
||||
// 기존 스케줄 확인
|
||||
$existing = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
|
||||
echo "<p><strong>기존 스케줄:</strong> {$existing['count']}개</p>";
|
||||
|
||||
// 충돌 검사
|
||||
// 💡 [수정] 최신 충돌 검사 로직으로 변경
|
||||
$conflicts = $generator->findConflictsWithNewSettings($year, $month);
|
||||
echo "<p><strong>충돌 검사:</strong> " . count($conflicts) . "건</p>";
|
||||
|
||||
if (!empty($conflicts)) {
|
||||
echo "<ul>";
|
||||
foreach ($conflicts as $conflict) {
|
||||
echo "<li>{$conflict['date']} {$conflict['time']} - {$conflict['customer']} ({$conflict['reason']})</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
|
||||
// 스케줄 생성
|
||||
$result = $generator->generateMonth($year, $month);
|
||||
|
||||
if ($result) {
|
||||
$new_count = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
|
||||
echo "<p style='color: green;'><strong>✅ 성공:</strong> {$new_count['count']}개 스케줄 생성 완료</p>";
|
||||
|
||||
// 생성된 스케줄 샘플 표시
|
||||
$samples = sql_query("SELECT * FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month} ORDER BY specific_date, start_time LIMIT 10");
|
||||
echo "<h4>생성된 스케줄 샘플 (최대 10개)</h4>";
|
||||
echo "<table border='1' style='border-collapse: collapse; width: 100%;'>";
|
||||
echo "<tr><th>날짜</th><th>시작시간</th><th>종료시간</th><th>최대인원</th><th>사용가능</th><th>타입</th></tr>";
|
||||
while ($row = sql_fetch_array($samples)) {
|
||||
$available = $row['is_available'] ? '가능' : '불가능';
|
||||
$type = $row['temp_1'] ?? '일반';
|
||||
echo "<tr>";
|
||||
echo "<td>{$row['specific_date']}</td>";
|
||||
echo "<td>{$row['start_time']}</td>";
|
||||
echo "<td>{$row['end_time']}</td>";
|
||||
echo "<td>{$row['max_persons']}</td>";
|
||||
echo "<td>{$available}</td>";
|
||||
echo "<td>{$type}</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
echo "</table>";
|
||||
} else {
|
||||
echo "<p style='color: red;'><strong>❌ 실패:</strong> 스케줄 생성에 실패했습니다.</p>";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "<p style='color: red;'><strong>오류:</strong> " . htmlspecialchars($e->getMessage()) . "</p>";
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.test-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="test-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<div class="test-form">
|
||||
<h3>스케줄 생성 테스트</h3>
|
||||
<p>선택한 년월의 스케줄을 생성하고 결과를 확인합니다.</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="test_generation">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="year">년도</label>
|
||||
<input type="number" id="year" name="year" value="<?php echo date('Y'); ?>" min="2024" max="2030">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="month">월</label>
|
||||
<select id="month" name="month">
|
||||
<?php for ($i = 1; $i <= 12; $i++): ?>
|
||||
<option value="<?php echo $i; ?>" <?php echo $i == date('n') ? 'selected' : ''; ?>>
|
||||
<?php echo $i; ?>월
|
||||
</option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-primary">스케줄 생성 테스트</button>
|
||||
<a href="../settings.php" class="btn btn-secondary">설정으로 돌아가기</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* ScheduleGenerator 테스트 스크립트
|
||||
*/
|
||||
|
||||
// 기본 설정
|
||||
define('G5_PATH', realpath('../../'));
|
||||
include_once(G5_PATH . '/common.php');
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
die('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
die('상담 예약 시스템이 설치되지 않았습니다.');
|
||||
}
|
||||
|
||||
echo "<h2>ScheduleGenerator 테스트</h2>";
|
||||
|
||||
try {
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
$generator = new ScheduleGenerator();
|
||||
|
||||
// 테스트할 년월
|
||||
$year = 2024;
|
||||
$month = 12;
|
||||
|
||||
echo "<h3>1. 기본 설정 확인</h3>";
|
||||
|
||||
// 기본 설정 확인
|
||||
$duration = consultant_get_config('consultation_duration', 60);
|
||||
$maxPersons = consultant_get_config('max_persons_per_slot', 2);
|
||||
|
||||
echo "- 상담 시간: {$duration}분<br>";
|
||||
echo "- 최대 인원: {$maxPersons}명<br>";
|
||||
|
||||
echo "<h3>2. 요일별 설정 확인</h3>";
|
||||
|
||||
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
$dayNames = ['월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
|
||||
|
||||
foreach ($days as $i => $day) {
|
||||
$enabled = consultant_get_config($day . '_enabled', '1');
|
||||
$start = consultant_get_config($day . '_start', '09:00');
|
||||
$end = consultant_get_config($day . '_end', '18:00');
|
||||
$lunchStart = consultant_get_config($day . '_lunch_start', '12:00');
|
||||
$lunchEnd = consultant_get_config($day . '_lunch_end', '13:00');
|
||||
|
||||
echo "- {$dayNames[$i]}: ";
|
||||
if ($enabled == '1') {
|
||||
echo "운영 ({$start}~{$end}, 점심: {$lunchStart}~{$lunchEnd})";
|
||||
} else {
|
||||
echo "휴무";
|
||||
}
|
||||
echo "<br>";
|
||||
}
|
||||
|
||||
echo "<h3>3. 기존 스케줄 확인</h3>";
|
||||
|
||||
$existingCount = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
|
||||
echo "- 기존 스케줄: {$existingCount['count']}개<br>";
|
||||
|
||||
echo "<h3>4. 충돌 검사</h3>";
|
||||
|
||||
$conflicts = $generator->checkScheduleConflicts($year, $month);
|
||||
echo "- 충돌 건수: " . count($conflicts) . "건<br>";
|
||||
|
||||
if (!empty($conflicts)) {
|
||||
echo "<ul>";
|
||||
foreach ($conflicts as $conflict) {
|
||||
echo "<li>{$conflict['date']} {$conflict['time']} - {$conflict['customer']} ({$conflict['reason']})</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
|
||||
echo "<h3>5. 스케줄 생성 테스트</h3>";
|
||||
|
||||
$result = $generator->generateMonth($year, $month);
|
||||
|
||||
if ($result) {
|
||||
$newCount = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
|
||||
echo "<p style='color: green;'>✅ 성공: {$newCount['count']}개 스케줄 생성 완료</p>";
|
||||
|
||||
// 생성된 스케줄 샘플 표시
|
||||
echo "<h4>생성된 스케줄 샘플 (첫 5일)</h4>";
|
||||
$samples = sql_query("SELECT * FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month} ORDER BY specific_date, start_time LIMIT 20");
|
||||
|
||||
echo "<table border='1' style='border-collapse: collapse; width: 100%;'>";
|
||||
echo "<tr><th>날짜</th><th>요일</th><th>시작시간</th><th>종료시간</th><th>최대인원</th><th>사용가능</th><th>타입</th><th>메모</th></tr>";
|
||||
|
||||
while ($row = sql_fetch_array($samples)) {
|
||||
$dayOfWeek = date('N', strtotime($row['specific_date']));
|
||||
$dayName = $dayNames[$dayOfWeek - 1];
|
||||
$available = $row['is_available'] ? '가능' : '불가능';
|
||||
$type = $row['temp_1'] ?? '일반';
|
||||
$memo = $row['temp_2'] ?? '';
|
||||
|
||||
echo "<tr>";
|
||||
echo "<td>{$row['specific_date']}</td>";
|
||||
echo "<td>{$dayName}</td>";
|
||||
echo "<td>{$row['start_time']}</td>";
|
||||
echo "<td>{$row['end_time']}</td>";
|
||||
echo "<td>{$row['max_persons']}</td>";
|
||||
echo "<td>{$available}</td>";
|
||||
echo "<td>{$type}</td>";
|
||||
echo "<td>{$memo}</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
echo "</table>";
|
||||
|
||||
// 타입별 통계
|
||||
echo "<h4>생성된 스케줄 통계</h4>";
|
||||
$stats = sql_query("SELECT temp_1, COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month} GROUP BY temp_1");
|
||||
|
||||
echo "<ul>";
|
||||
while ($stat = sql_fetch_array($stats)) {
|
||||
$type = $stat['temp_1'] ?? '일반';
|
||||
echo "<li>{$type}: {$stat['count']}개</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
|
||||
} else {
|
||||
echo "<p style='color: red;'>❌ 실패: 스케줄 생성에 실패했습니다.</p>";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "<p style='color: red;'>오류: " . htmlspecialchars($e->getMessage()) . "</p>";
|
||||
echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
echo "<p><a href='../settings.php'>설정으로 돌아가기</a> | <a href='../schedule.php'>스케줄 관리</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 테스트 페이지
|
||||
*/
|
||||
|
||||
$sub_menu = '850900';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 권한 확인
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$g5['title'] = '시스템 테스트';
|
||||
|
||||
// 테스트 결과
|
||||
$test_results = [];
|
||||
|
||||
// 1. 테이블 존재 확인
|
||||
$tables_to_check = [
|
||||
'consultant_config' => '설정 테이블',
|
||||
'consultant_schedule' => '스케줄 테이블',
|
||||
'consultant_reservations' => '예약 테이블',
|
||||
'consultant_mail_templates' => '메일 템플릿 테이블',
|
||||
'consultant_sms_templates' => 'SMS 템플릿 테이블'
|
||||
];
|
||||
|
||||
foreach ($tables_to_check as $table => $description) {
|
||||
$sql = "SHOW TABLES LIKE '{$table}'";
|
||||
$result = sql_query($sql, false);
|
||||
$exists = $result && sql_num_rows($result) > 0;
|
||||
|
||||
$test_results['tables'][$table] = [
|
||||
'name' => $description,
|
||||
'status' => $exists,
|
||||
'message' => $exists ? '존재함' : '존재하지 않음'
|
||||
];
|
||||
}
|
||||
|
||||
// 2. 기본 설정값 확인
|
||||
$config_keys = [
|
||||
'consultation_duration' => '1회 상담시간',
|
||||
'max_persons_per_slot' => '최대 인원',
|
||||
'consultation_fee' => '상담비',
|
||||
'account_info' => '계좌정보',
|
||||
'monday_enabled' => '월요일 운영여부',
|
||||
'monday_start' => '월요일 시작시간'
|
||||
];
|
||||
|
||||
foreach ($config_keys as $key => $description) {
|
||||
$value = consultant_get_config($key);
|
||||
$test_results['config'][$key] = [
|
||||
'name' => $description,
|
||||
'value' => $value,
|
||||
'status' => $value !== null
|
||||
];
|
||||
}
|
||||
|
||||
// 3. 함수 테스트
|
||||
$function_tests = [
|
||||
'is_consultant_installed' => is_consultant_installed(),
|
||||
'consultant_get_config' => consultant_get_config('consultation_duration', '60'),
|
||||
'consultant_format_time' => consultant_format_time('14:30'),
|
||||
'consultant_format_date' => consultant_format_date('2024-12-02')
|
||||
];
|
||||
|
||||
// 4. 스케줄 생성 테스트 (현재 월)
|
||||
$current_year = date('Y');
|
||||
$current_month = date('m');
|
||||
|
||||
try {
|
||||
include_once('./schedule_generator.php');
|
||||
$generator = new ScheduleGenerator();
|
||||
$schedule_status = $generator->checkScheduleStatus($current_year, $current_month);
|
||||
$test_results['schedule_status'] = $schedule_status;
|
||||
} catch (Exception $e) {
|
||||
$test_results['schedule_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.test-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.test-section h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.test-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.test-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.test-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-value {
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.test-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="test-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<!-- 전체 요약 -->
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<div class="summary-number" style="color: <?php echo is_consultant_installed() ? '#28a745' : '#dc3545'; ?>">
|
||||
<?php echo is_consultant_installed() ? '✓' : '✗'; ?>
|
||||
</div>
|
||||
<div class="summary-label">시스템 설치</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card">
|
||||
<div class="summary-number" style="color: #007bff;">
|
||||
<?php echo count(array_filter($test_results['tables'], function ($t) {
|
||||
return $t['status']; })); ?>/<?php echo count($test_results['tables']); ?>
|
||||
</div>
|
||||
<div class="summary-label">테이블 상태</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card">
|
||||
<div class="summary-number" style="color: #17a2b8;">
|
||||
<?php echo count(array_filter($test_results['config'], function ($c) {
|
||||
return $c['status']; })); ?>/<?php echo count($test_results['config']); ?>
|
||||
</div>
|
||||
<div class="summary-label">설정 상태</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card">
|
||||
<div class="summary-number" style="color: #6f42c1;">
|
||||
<?php echo isset($test_results['schedule_status']) ? number_format($test_results['schedule_status']['total_slots']) : '0'; ?>
|
||||
</div>
|
||||
<div class="summary-label">현재 월 스케줄</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 테이블 상태 -->
|
||||
<div class="test-section">
|
||||
<h3>📋 데이터베이스 테이블 상태</h3>
|
||||
<?php foreach ($test_results['tables'] as $table => $info): ?>
|
||||
<div class="test-item">
|
||||
<div class="test-name"><?php echo $info['name']; ?> (<?php echo $table; ?>)</div>
|
||||
<div class="test-status <?php echo $info['status'] ? 'status-ok' : 'status-error'; ?>">
|
||||
<?php echo $info['message']; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- 설정 상태 -->
|
||||
<div class="test-section">
|
||||
<h3>⚙️ 기본 설정 상태</h3>
|
||||
<?php foreach ($test_results['config'] as $key => $info): ?>
|
||||
<div class="test-item">
|
||||
<div class="test-name"><?php echo $info['name']; ?> (<?php echo $key; ?>)</div>
|
||||
<div class="test-value"><?php echo htmlspecialchars($info['value'] ?? 'NULL'); ?></div>
|
||||
<div class="test-status <?php echo $info['status'] ? 'status-ok' : 'status-warning'; ?>">
|
||||
<?php echo $info['status'] ? '설정됨' : '미설정'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- 함수 테스트 -->
|
||||
<div class="test-section">
|
||||
<h3>🔧 함수 테스트</h3>
|
||||
<?php foreach ($function_tests as $func_name => $result): ?>
|
||||
<div class="test-item">
|
||||
<div class="test-name"><?php echo $func_name; ?>()</div>
|
||||
<div class="test-value">
|
||||
<?php echo is_bool($result) ? ($result ? 'true' : 'false') : htmlspecialchars($result); ?></div>
|
||||
<div class="test-status status-ok">정상</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- 스케줄 상태 -->
|
||||
<div class="test-section">
|
||||
<h3>📅 현재 월 스케줄 상태 (<?php echo $current_year; ?>년 <?php echo $current_month; ?>월)</h3>
|
||||
|
||||
<?php if (isset($test_results['schedule_status'])): ?>
|
||||
<?php $status = $test_results['schedule_status']; ?>
|
||||
<div class="test-item">
|
||||
<div class="test-name">전체 슬롯</div>
|
||||
<div class="test-value"><?php echo number_format($status['total_slots']); ?>개</div>
|
||||
<div class="test-status <?php echo $status['total_slots'] > 0 ? 'status-ok' : 'status-warning'; ?>">
|
||||
<?php echo $status['total_slots'] > 0 ? '생성됨' : '미생성'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<div class="test-name">상담 가능 슬롯</div>
|
||||
<div class="test-value"><?php echo number_format($status['auto_slots']); ?>개</div>
|
||||
<div class="test-status status-ok">정상</div>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<div class="test-name">점심시간 슬롯</div>
|
||||
<div class="test-value"><?php echo number_format($status['lunch_slots']); ?>개</div>
|
||||
<div class="test-status status-ok">정상</div>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<div class="test-name">휴무일 슬롯</div>
|
||||
<div class="test-value"><?php echo number_format($status['holiday_slots']); ?>개</div>
|
||||
<div class="test-status status-ok">정상</div>
|
||||
</div>
|
||||
|
||||
<?php elseif (isset($test_results['schedule_error'])): ?>
|
||||
<div class="test-item">
|
||||
<div class="test-name">스케줄 조회 오류</div>
|
||||
<div class="test-value"><?php echo htmlspecialchars($test_results['schedule_error']); ?></div>
|
||||
<div class="test-status status-error">오류</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="test-section">
|
||||
<h3>🚀 빠른 액션</h3>
|
||||
<div style="text-align: center;">
|
||||
<?php if (!is_consultant_installed()): ?>
|
||||
<a href="../install.php" class="btn btn-warning">시스템 설치</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="../settings.php" class="btn btn-primary">설정 관리</a>
|
||||
<a href="../schedule_generate.php" class="btn btn-success">스케줄 생성</a>
|
||||
<a href="../dashboard.php" class="btn btn-primary">대시보드</a>
|
||||
|
||||
<button onclick="location.reload()" class="btn btn-secondary">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 자동 새로고침 (30초마다)
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 30000);
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
echo '<div style="padding:20px; border:1px solid #ddd; background:#fff; max-width:600px; margin:50px auto; text-align:center;">';
|
||||
echo '<h3>알림</h3>';
|
||||
echo '<p>이 파일은 더 이상 사용되지 않습니다.</p>';
|
||||
echo '<p>관리자 페이지 > 상담관리 > <strong>설치/업데이트</strong> 메뉴에서 [재설치 (업데이트)] 버튼을 클릭하여 DB를 업데이트해 주세요.</p>';
|
||||
echo '<a href="install.php" style="display:inline-block; padding:10px 20px; background:#007bff; color:#fff; text-decoration:none; border-radius:5px;">설치/업데이트 페이지로 이동</a>';
|
||||
echo '</div>';
|
||||
?>
|
||||
Reference in New Issue
Block a user