first commit 2

This commit is contained in:
hmw1001
2026-06-11 18:47:38 +09:00
parent c768729ce6
commit 6f534e33a6
11095 changed files with 1595758 additions and 0 deletions
+12
View File
@@ -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');
?>
+9
View File
@@ -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="팝업 닫기">&times;</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>&times;</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>&lt;?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?&gt;</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');
?>
+578
View File
@@ -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');
?>
+128
View File
@@ -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;
}
?>
+209
View File
@@ -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');
?>
+159
View File
@@ -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];
}
}
+529
View File
@@ -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.'&amp;page='); ?>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
-
+500
View File
@@ -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">&times;</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');
?>
+326
View File
@@ -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')">&times;</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')">&times;</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');
?>
+85
View File
@@ -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>&lt;?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?&gt;</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');
?>
+293
View File
@@ -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');
?>
+454
View File
@@ -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">&times;</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);
}
?>
+381
View File
@@ -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');
?>
+454
View File
@@ -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');
?>
+427
View File
@@ -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">관리자 &lt;admin@example.com&gt;</span></div>
<div class="meta-item"><span class="meta-label">받는사람:</span><span class="meta-value">홍길동 &lt;hong@example.com&gt;</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');
?>
+65
View File
@@ -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>";
?>
+345
View File
@@ -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>';
?>