first commit 2
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 공통 파일 (관리자용)
|
||||
*/
|
||||
|
||||
define('G5_IS_ADMIN', true);
|
||||
include_once('../../common.php');
|
||||
include_once(G5_ADMIN_PATH . '/admin.lib.php');
|
||||
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
// AJAX 요청 등으로 직접 접근했을 때, 그누보드 환경을 로드합니다.
|
||||
include_once('../../../common.php');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// 800번대 최상위 메뉴 '견적 관리'를 정의합니다.
|
||||
$menu['menu800'][] = array('800000', '견적 관리', G5_ADMIN_URL . '/order_manage/statistics.php', 'order_manage', 'fa-file-text-o');
|
||||
|
||||
// '견적 관리'의 하위 메뉴들을 정의합니다.
|
||||
$menu['menu800'][] = array('800100', '견적 목록', G5_BBS_URL . '/board.php?bo_table=order', 'order_list');
|
||||
$menu['menu800'][] = array('800200', '견적 통계', G5_ADMIN_URL . '/order_manage/statistics.php', 'order_statistics');
|
||||
$menu['menu800'][] = array('800300', '시스템 설정', G5_ADMIN_URL . '/order_manage/config_manager.php', 'order_config');
|
||||
$menu['menu800'][] = array('800400', '전문가 방문', G5_ADMIN_URL . '/order_manage/expert_visits.php', 'expert_visits');
|
||||
$menu['menu800'][] = array('800500', '메일 템플릿', G5_ADMIN_URL . '/order_manage/mail_templates.php', 'mail_templates');
|
||||
$menu['menu800'][] = array('800600', 'SMS 템플릿', G5_ADMIN_URL . '/order_manage/sms_templates.php', 'sms_templates');
|
||||
$menu['menu800'][] = array('800700', '알림 로그 관리', G5_ADMIN_URL.'/order_manage/notification_log.php', 'notification_log');
|
||||
$menu['menu800'][] = array('800800', '전문가 방문 예약', G5_ADMIN_URL.'/order_manage/expert_visit_reservations.php', 'expert_visit_reservations');
|
||||
$menu['menu800'][] = array('800850', '전문가 방문 스케줄', G5_ADMIN_URL.'/order_manage/expert_visit_schedule.php', 'expert_visit_schedule');
|
||||
$menu['menu800'][] = array('800860', '윈도우 창호 관리', G5_ADMIN_URL.'/order_manage/brand_manager.php', 'brand_manager');
|
||||
$menu['menu800'][] = array('800900', '솔루션 설치', G5_ADMIN_URL . '/order_manage/install.php', 'order_solution_install');
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
$sub_menu = "800860";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '창호 브랜드 관리';
|
||||
include_once(G5_ADMIN_PATH.'/admin.head.php');
|
||||
|
||||
$sql_common = " FROM `order_window_brands` ";
|
||||
$sql_search = " WHERE is_deleted = 0 ";
|
||||
|
||||
// 검색어 처리
|
||||
$stx = isset($_GET['stx']) ? clean_xss_tags(trim($_GET['stx'])) : '';
|
||||
if ($stx) {
|
||||
$sql_search .= " AND ( brand_name LIKE '%{$stx}%' OR brand_code LIKE '%{$stx}%' OR manufacturer LIKE '%{$stx}%' ) ";
|
||||
}
|
||||
|
||||
$sql_order = " ORDER BY sort_order ASC, brand_name ASC ";
|
||||
|
||||
$sql = " SELECT COUNT(*) AS cnt {$sql_common} {$sql_search} ";
|
||||
$row = sql_fetch($sql);
|
||||
$total_count = $row['cnt'];
|
||||
|
||||
$rows = $config['cf_page_rows'];
|
||||
$total_page = ceil($total_count / $rows);
|
||||
if ($page < 1) $page = 1;
|
||||
$from_record = ($page - 1) * $rows;
|
||||
|
||||
$sql = " SELECT * {$sql_common} {$sql_search} {$sql_order} LIMIT {$from_record}, {$rows} ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
$list_num = $total_count - ($page - 1) * $rows;
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
대리점이 견적서 작성 시 선택할 수 있는 창호 브랜드를 관리합니다.<br>
|
||||
정렬 숫자가 낮을수록 목록 상단에 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<label for="stx" class="sound_only">검색어</label>
|
||||
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" class="frm_input" placeholder="브랜드명, 코드, 제조사">
|
||||
<input type="submit" value="검색" class="btn_submit">
|
||||
</form>
|
||||
|
||||
<form name="fbrandlist" id="fbrandlist" method="post" action="./brand_manager_update.php" autocomplete="off">
|
||||
<input type="hidden" name="page" value="<?php echo $page; ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx; ?>">
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<label for="chkall" class="sound_only">브랜드 전체</label>
|
||||
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
|
||||
</th>
|
||||
<th scope="col">브랜드명</th>
|
||||
<th scope="col">브랜드 코드</th>
|
||||
<th scope="col">제조사</th>
|
||||
<th scope="col">정렬</th>
|
||||
<th scope="col">사용</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
for ($i=0; $row=sql_fetch_array($result); $i++) {
|
||||
$bg = 'bg'.($i%2);
|
||||
?>
|
||||
<tr class="<?php echo $bg; ?>">
|
||||
<td class="td_chk">
|
||||
<input type="hidden" name="id[<?php echo $i ?>]" value="<?php echo $row['id'] ?>">
|
||||
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo get_text($row['brand_name']); ?> 브랜드</label>
|
||||
<input type="checkbox" name="chk[]" value="<?php echo $i ?>" id="chk_<?php echo $i ?>">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="brand_name[<?php echo $i ?>]" value="<?php echo get_text($row['brand_name']) ?>" class="frm_input" size="15" required>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="brand_code[<?php echo $i ?>]" value="<?php echo get_text($row['brand_code']) ?>" class="frm_input" size="10">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="manufacturer[<?php echo $i ?>]" value="<?php echo get_text($row['manufacturer']) ?>" class="frm_input" size="15">
|
||||
</td>
|
||||
<td class="td_num">
|
||||
<input type="number" name="sort_order[<?php echo $i ?>]" value="<?php echo get_text($row['sort_order']) ?>" class="frm_input" size="3">
|
||||
</td>
|
||||
<td class="td_chk">
|
||||
<input type="checkbox" name="is_used[<?php echo $i ?>]" value="1" id="is_used_<?php echo $i ?>" <?php echo $row['is_used']?'checked':''; ?>>
|
||||
<label for="is_used_<?php echo $i; ?>" class="sound_only">사용</label>
|
||||
</td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<a href="./brand_manager_update.php?w=d&id=<?php echo $row['id']; ?>&page=<?php echo $page; ?>&stx=<?php echo $stx; ?>&token=<?php echo $token; ?>" onclick="return delete_confirm(this);" class="btn btn_02">삭제</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
if ($i == 0)
|
||||
echo '<tr><td colspan="7" class="empty_table">자료가 없습니다.</td></tr>';
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_fixed_top">
|
||||
<input type="submit" name="act_button" value="선택수정" onclick="document.pressed=this.value" class="btn btn_02">
|
||||
<input type="submit" name="act_button" value="선택삭제" onclick="document.pressed=this.value" class="btn btn_02">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<form name="fbrandadd" id="fbrandadd" method="post" action="./brand_manager_update.php" autocomplete="off" class="form_01">
|
||||
<input type="hidden" name="page" value="<?php echo $page; ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx; ?>">
|
||||
|
||||
<h2>브랜드 추가</h2>
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>브랜드 추가</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="brand_name">브랜드명<strong class="sound_only">필수</strong></label></th>
|
||||
<td><input type="text" name="brand_name" id="brand_name" required class="required frm_input" size="30"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="brand_code">브랜드 코드</label></th>
|
||||
<td><input type="text" name="brand_code" id="brand_code" class="frm_input" size="15"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="manufacturer">제조사</label></th>
|
||||
<td><input type="text" name="manufacturer" id="manufacturer" class="frm_input" size="30"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sort_order">정렬</label></th>
|
||||
<td><input type="number" name="sort_order" value="0" id="sort_order" class="frm_input" size="5"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="is_used">사용여부</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="is_used" value="1" id="is_used" checked>
|
||||
<label for="is_used">사용</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" name="act_button" value="추가" class="btn_submit">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<?php
|
||||
echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$qstr&page=");
|
||||
|
||||
include_once(G5_ADMIN_PATH.'/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
$sub_menu = "800700";
|
||||
include_once('./_common.php');
|
||||
|
||||
if ($w == 'd')
|
||||
auth_check($auth[$sub_menu], 'd');
|
||||
else
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$act_button = isset($_POST['act_button']) ? $_POST['act_button'] : '';
|
||||
|
||||
if ($act_button == "선택수정") {
|
||||
|
||||
for ($i=0; $i<count($_POST['chk']); $i++) {
|
||||
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
|
||||
$id = isset($_POST['id'][$k]) ? (int) $_POST['id'][$k] : 0;
|
||||
|
||||
$sql = "UPDATE `order_window_brands`
|
||||
SET brand_name = '".sql_real_escape_string($_POST['brand_name'][$k])."',
|
||||
brand_code = '".sql_real_escape_string($_POST['brand_code'][$k])."',
|
||||
manufacturer = '".sql_real_escape_string($_POST['manufacturer'][$k])."',
|
||||
sort_order = '".(int)($_POST['sort_order'][$k])."',
|
||||
is_used = '".(isset($_POST['is_used'][$k]) ? 1 : 0)."'
|
||||
WHERE id = '{$id}'";
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
} else if ($act_button == "선택삭제") {
|
||||
|
||||
for ($i=0; $i<count($_POST['chk']); $i++) {
|
||||
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
|
||||
$id = isset($_POST['id'][$k]) ? (int) $_POST['id'][$k] : 0;
|
||||
|
||||
// 소프트 삭제 (is_deleted 플래그 사용)
|
||||
$sql = "UPDATE `order_window_brands` SET is_deleted = 1 WHERE id = '{$id}'";
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
} else if ($act_button == "추가") {
|
||||
|
||||
$brand_name = isset($_POST['brand_name']) ? trim(strip_tags(clean_xss_attributes($_POST['brand_name']))) : '';
|
||||
if (!$brand_name) {
|
||||
alert('브랜드명을 입력해주세요.');
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
$sql = "SELECT COUNT(*) as cnt FROM `order_window_brands` WHERE brand_name = '".sql_real_escape_string($brand_name)."' AND is_deleted = 0";
|
||||
$row = sql_fetch($sql);
|
||||
if ($row['cnt']) {
|
||||
alert('이미 등록된 브랜드명입니다.');
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO `order_window_brands`
|
||||
SET brand_name = '".sql_real_escape_string($brand_name)."',
|
||||
brand_code = '".sql_real_escape_string($_POST['brand_code'])."',
|
||||
manufacturer = '".sql_real_escape_string($_POST['manufacturer'])."',
|
||||
sort_order = '".(int)($_POST['sort_order'])."',
|
||||
is_used = '".(isset($_POST['is_used']) ? 1 : 0)."',
|
||||
is_deleted = 0,
|
||||
created_at = NOW(),
|
||||
updated_at = NOW()";
|
||||
sql_query($sql);
|
||||
|
||||
} else if ($w == 'd') {
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if (!$id) {
|
||||
alert('잘못된 접근입니다.');
|
||||
}
|
||||
|
||||
// 소프트 삭제 (is_deleted 플래그 사용)
|
||||
$sql = "UPDATE `order_window_brands` SET is_deleted = 1 WHERE id = '{$id}'";
|
||||
sql_query($sql);
|
||||
|
||||
} else {
|
||||
alert('잘못된 접근입니다.');
|
||||
}
|
||||
|
||||
$qstr = "page={$page}&stx={$stx}";
|
||||
goto_url("./brand_manager.php?{$qstr}");
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
class FormManager
|
||||
{
|
||||
private $ui_manager_table;
|
||||
private $form_category_table;
|
||||
private $common_lang_table;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
global $g5;
|
||||
$this->ui_manager_table = $g5['ui_manager_table'];
|
||||
$this->form_category_table = $g5['form_category_table'];
|
||||
$this->common_lang_table = $g5['common_lang_table'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 화면의 모든 UI 리소스를 가져와 구조화된 배열로 반환합니다.
|
||||
* @param string $screen_code 가져올 화면 코드 (e.g., 'order_form')
|
||||
* @param string $lang_code 언어 코드 (기본값 'ko')
|
||||
* @return array 구조화된 리소스 배열
|
||||
*/
|
||||
public function getResources($screen_code, $lang_code = 'ko')
|
||||
{
|
||||
$sql = "SELECT
|
||||
A.group_code,
|
||||
A.resource_code,
|
||||
A.resource_type,
|
||||
A.resource_desc,
|
||||
B.cl_name AS label,
|
||||
B.cl_description AS tooltip,
|
||||
C.fc_key,
|
||||
C.fc_order,
|
||||
D.cl_name AS option_name
|
||||
FROM {$this->ui_manager_table} AS A
|
||||
LEFT JOIN {$this->common_lang_table} AS B
|
||||
ON (A.um_id = B.target_id AND B.target_table = '{$this->ui_manager_table}' AND B.lang_code = '{$lang_code}')
|
||||
LEFT JOIN {$this->form_category_table} AS C
|
||||
ON (A.um_id = C.um_id AND C.is_deleted = 0 AND C.is_used = 1)
|
||||
LEFT JOIN {$this->common_lang_table} AS D
|
||||
ON (C.fc_id = D.target_id AND D.target_table = '{$this->form_category_table}' AND D.lang_code = '{$lang_code}')
|
||||
WHERE A.screen_code = '" . sql_real_escape_string($screen_code) . "' AND A.is_used = 1
|
||||
ORDER BY A.group_code, A.resource_code, C.fc_order, C.fc_id";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$resources = [];
|
||||
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$group = $row['group_code'];
|
||||
$code = $row['resource_code'];
|
||||
|
||||
if (!isset($resources[$group][$code])) {
|
||||
$resources[$group][$code] = [
|
||||
'type' => $row['resource_type'],
|
||||
'desc' => $row['resource_desc'],
|
||||
'label' => $row['label'],
|
||||
'tooltip' => $row['tooltip'],
|
||||
'options' => [],
|
||||
];
|
||||
}
|
||||
|
||||
if ($row['resource_type'] === 'DATA' && $row['fc_key']) {
|
||||
$resources[$group][$code]['options'][] = [
|
||||
'key' => $row['fc_key'],
|
||||
'name' => $row['option_name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
/**
|
||||
* 견적 상태 관리 클래스
|
||||
*
|
||||
* 견적 상태 변경, 권한 검증, 이력 관리를 담당합니다.
|
||||
*/
|
||||
class StatusManager
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
global $g5;
|
||||
$this->db = $g5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 코드를 화면 표시용 이름으로 변환합니다.
|
||||
* @param string $status_code 실제 상태 코드
|
||||
* @return string 화면 표시용 이름
|
||||
*/
|
||||
public static function get_status_display_name($status_code)
|
||||
{
|
||||
$config_key = $status_code;
|
||||
$result = sql_fetch("SELECT config_value FROM order_config WHERE config_key = '" . sql_real_escape_string($config_key) . "'");
|
||||
return $result ? $result['config_value'] : $status_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 역할별 상태 전환 규칙
|
||||
*/
|
||||
private function getStatusTransitions($user_role)
|
||||
{
|
||||
$customer_flow = [
|
||||
'견적신청중' => ['작성완료'],
|
||||
'작성완료' => [], // 고객은 직접 변경 불가. '견적채택' 시 자동으로 '계약금입금예정'으로 변경됨.
|
||||
'견적채택' => [],
|
||||
'계약금입금예정' => [],
|
||||
'계약금입금완료' => [],
|
||||
'중도금입금예정' => [],
|
||||
'중도금입금완료' => [],
|
||||
'잔금입금예정' => [],
|
||||
'잔금처리완료' => [],
|
||||
'시공완료' => []
|
||||
];
|
||||
|
||||
$agent_flow = [
|
||||
'견적제안' => [], // 대리점은 제안만 가능. 채택/취소는 고객이 함.
|
||||
'견적채택' => ['시공완료'], // 시공 완료 후 상태 변경
|
||||
'견적취소' => [],
|
||||
'시공완료' => []
|
||||
];
|
||||
|
||||
// 관리자는 모든 상태로 변경 가능하도록 모든 상태를 나열합니다.
|
||||
$all_statuses = array_keys(get_all_status_names());
|
||||
$admin_flow = [];
|
||||
foreach ($all_statuses as $status) {
|
||||
$admin_flow[$status] = $all_statuses;
|
||||
}
|
||||
|
||||
$transitions = [
|
||||
'customer' => $customer_flow,
|
||||
'agent' => $agent_flow,
|
||||
'admin' => $admin_flow
|
||||
];
|
||||
|
||||
// [예외 규칙] 고객은 대리점의 '견적제안'을 '견적채택' 할 수 있습니다.
|
||||
if ($user_role === 'customer') {
|
||||
$transitions['customer']['견적제안'] = ['견적채택'];
|
||||
}
|
||||
|
||||
return $transitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 전환 유효성 검증
|
||||
*/
|
||||
public function validateStatusTransition($current_status, $new_status, $user_role, $wr_id = null)
|
||||
{
|
||||
if ($user_role === 'admin') {
|
||||
return ['valid' => true, 'message' => '관리자 권한으로 상태를 변경합니다.'];
|
||||
}
|
||||
|
||||
$transitions = $this->getStatusTransitions($user_role);
|
||||
|
||||
if (!isset($transitions[$current_status])) {
|
||||
return ['valid' => false, 'message' => "현재 상태 '{$current_status}'에서는 상태 변경이 불가능합니다."];
|
||||
}
|
||||
|
||||
if (!in_array($new_status, $transitions[$current_status])) {
|
||||
return ['valid' => false, 'message' => "'{$current_status}'에서 '{$new_status}'로 변경할 수 없습니다."];
|
||||
}
|
||||
|
||||
return ['valid' => true, 'message' => '상태 변경이 가능합니다.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경 실행
|
||||
*/
|
||||
public function changeStatus($wr_id, $new_status, $user_id, $user_role, $memo = '')
|
||||
{
|
||||
try {
|
||||
$current_data = $this->getCurrentStatus($wr_id);
|
||||
if (!$current_data['success']) {
|
||||
return $current_data;
|
||||
}
|
||||
$current_status = $current_data['data']['status'];
|
||||
|
||||
$validation = $this->validateStatusTransition($current_status, $new_status, $user_role, $wr_id);
|
||||
if (!$validation['valid']) {
|
||||
return ['success' => false, 'message' => $validation['message']];
|
||||
}
|
||||
|
||||
sql_query("BEGIN");
|
||||
|
||||
$update_result = $this->updateStatus($wr_id, $new_status, $user_id);
|
||||
if (!$update_result['success']) {
|
||||
sql_query("ROLLBACK");
|
||||
return $update_result;
|
||||
}
|
||||
|
||||
$history_result = $this->logStatusChange($wr_id, $current_status, $new_status, $user_id, $memo);
|
||||
if (!$history_result['success']) {
|
||||
sql_query("ROLLBACK");
|
||||
return $history_result;
|
||||
}
|
||||
|
||||
// ❗ [핵심 수정] 후속 처리 로직 호출 복원
|
||||
$post_process_result = $this->postProcessStatusChange($wr_id, $current_status, $new_status, $user_id);
|
||||
if (!$post_process_result['success']) {
|
||||
// 후속 처리 실패는 경고만 하고 롤백하지 않음
|
||||
error_log("Status change post-process warning: " . $post_process_result['message']);
|
||||
}
|
||||
|
||||
sql_query("COMMIT");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "상태가 '" . self::get_status_display_name($current_status) . "'에서 '" . self::get_status_display_name($new_status) . "'로 변경되었습니다."
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
sql_query("ROLLBACK");
|
||||
error_log("StatusManager::changeStatus Error: " . $e->getMessage());
|
||||
return ['success' => false, 'message' => '상태 변경 중 오류가 발생했습니다: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 상태 조회
|
||||
*/
|
||||
public function getCurrentStatus($wr_id)
|
||||
{
|
||||
|
||||
$sql = "SELECT wr_reply FROM {$this->db['write_prefix']}order WHERE wr_id = '{$wr_id}'";
|
||||
$write_data = sql_fetch("SELECT wr_reply FROM {$this->db['write_prefix']}order WHERE wr_id = '{$wr_id}'");
|
||||
if (!$write_data) {
|
||||
return ['success' => false, 'message' => '게시물을 찾을 수 없습니다.'.'$wr_id => '.$wr_id.'$sql -> '.$sql];
|
||||
}
|
||||
|
||||
if (empty($write_data['wr_reply'])) { // 원본 글 (고객)
|
||||
$estimate = sql_fetch("SELECT status FROM estimate WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
||||
$status = $estimate ? $estimate['status'] : '견적신청중';
|
||||
} else { // 답변 글 (대리점)
|
||||
$reply_data = sql_fetch("SELECT wr_1 FROM {$this->db['write_prefix']}order WHERE wr_id = '{$wr_id}'");
|
||||
$status = $reply_data['wr_1'] ?: '견적제안';
|
||||
}
|
||||
|
||||
return ['success' => true, 'data' => ['status' => $status]];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트 실행
|
||||
*/
|
||||
private function updateStatus($wr_id, $new_status, $user_id)
|
||||
{
|
||||
// 🔥 [핵심 수정] 원본글/답변글 구분 없이 estimate 테이블의 status를 업데이트합니다.
|
||||
// estimate 테이블에 해당 wr_id의 레코드가 있는지 먼저 확인합니다.
|
||||
$estimate_exists = sql_fetch("SELECT id FROM estimate WHERE wr_id = '{$wr_id}'");
|
||||
|
||||
if ($estimate_exists) {
|
||||
$sql = "UPDATE estimate SET status = '" . sql_real_escape_string($new_status) . "', updated_at = NOW(), updated_by = '" . sql_real_escape_string($user_id) . "' WHERE wr_id = '{$wr_id}'";
|
||||
$result = sql_query($sql);
|
||||
if (!$result) {
|
||||
return ['success' => false, 'message' => '견적서 상태 업데이트 실패: ' . sql_error()];
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 [핵심 수정] 게시판의 wr_1 필드도 동기화합니다.
|
||||
$sql_board = "UPDATE {$this->db['write_prefix']}order SET wr_1 = '" . sql_real_escape_string($new_status) . "' WHERE wr_id = '{$wr_id}'";
|
||||
$result_board = sql_query($sql_board);
|
||||
if (!$result_board) {
|
||||
// This is not a critical failure, so maybe just log it.
|
||||
error_log("게시판(wr_1) 상태 동기화 실패: wr_id={$wr_id}, new_status={$new_status}");
|
||||
}
|
||||
return ['success' => true, 'message' => '상태 업데이트 성공'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경 이력 기록
|
||||
*/
|
||||
public function logStatusChange($wr_id, $old_status, $new_status, $user_id, $memo = '')
|
||||
{
|
||||
$write = sql_fetch("SELECT wr_num, wr_reply FROM {$this->db['write_prefix']}order WHERE wr_id = '{$wr_id}'");
|
||||
if (!$write) {
|
||||
return ['success' => false, 'message' => '이력 기록을 위한 게시물을 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
$origin_wr_id = $wr_id;
|
||||
if (!empty($write['wr_reply'])) {
|
||||
$origin_post = sql_fetch("SELECT wr_id FROM {$this->db['write_prefix']}order WHERE wr_num = '{$write['wr_num']}' AND wr_reply = ''");
|
||||
if ($origin_post) {
|
||||
$origin_wr_id = $origin_post['wr_id'];
|
||||
} else {
|
||||
return ['success' => false, 'message' => '이력 기록을 위한 원본 견적을 찾을 수 없습니다.'];
|
||||
}
|
||||
}
|
||||
|
||||
$estimate = sql_fetch("SELECT id FROM estimate WHERE wr_id = '{$origin_wr_id}'");
|
||||
if (!$estimate) {
|
||||
return ['success' => false, 'message' => '견적 정보를 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
$change_details = json_encode(['wr_id' => $wr_id, 'old_status' => $old_status, 'new_status' => $new_status, 'memo' => $memo, 'timestamp' => date('Y-m-d H:i:s')], JSON_UNESCAPED_UNICODE);
|
||||
$sql = "INSERT INTO estimate_history (estimate_id, action, change_details, changed_by, changed_at, created_at, created_by) VALUES ('{$estimate['id']}', 'status_change', '" . sql_real_escape_string($change_details) . "', '" . sql_real_escape_string($user_id) . "', NOW(), NOW(), '" . sql_real_escape_string($user_id) . "')";
|
||||
$result = sql_query($sql);
|
||||
|
||||
if (!$result) {
|
||||
return ['success' => false, 'message' => '이력 기록 실패: ' . sql_error()];
|
||||
}
|
||||
return ['success' => true, 'message' => '이력 기록 성공'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경 이력 조회
|
||||
*/
|
||||
public function getStatusHistory($wr_id, $limit = 10)
|
||||
{
|
||||
$write = sql_fetch("SELECT wr_num, wr_reply FROM {$this->db['write_prefix']}order WHERE wr_id = '{$wr_id}'");
|
||||
if (!$write) return [];
|
||||
|
||||
$origin_wr_id = $wr_id;
|
||||
if (!empty($write['wr_reply'])) {
|
||||
$origin_post = sql_fetch("SELECT wr_id FROM {$this->db['write_prefix']}order WHERE wr_num = '{$write['wr_num']}' AND wr_reply = ''");
|
||||
$origin_wr_id = $origin_post ? $origin_post['wr_id'] : 0;
|
||||
}
|
||||
|
||||
if (!$origin_wr_id) return [];
|
||||
|
||||
$estimate = sql_fetch("SELECT id FROM estimate WHERE wr_id = '{$origin_wr_id}'");
|
||||
if (!$estimate) return [];
|
||||
|
||||
$sql = "SELECT * FROM estimate_history WHERE estimate_id = '{$estimate['id']}' AND action = 'status_change' ORDER BY changed_at DESC LIMIT {$limit}";
|
||||
$result = sql_query($sql);
|
||||
$history = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$details = json_decode($row['change_details'], true);
|
||||
$history[] = [
|
||||
'id' => $row['id'],
|
||||
'changed_at' => $row['changed_at'],
|
||||
'changed_by' => $row['changed_by'],
|
||||
'old_status' => self::get_status_display_name($details['old_status'] ?? ''),
|
||||
'new_status' => self::get_status_display_name($details['new_status'] ?? ''),
|
||||
'memo' => $details['memo'] ?? ''
|
||||
];
|
||||
}
|
||||
return $history;
|
||||
}
|
||||
private function write_debug_log_ff($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}' 파일에 내용을 쓸 수 없습니다.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* ❗ [핵심 수정] 상태 변경 후 후속 처리
|
||||
*/
|
||||
private function postProcessStatusChange($wr_id, $old_status, $new_status, $user_id)
|
||||
{
|
||||
$this->write_debug_log_ff("[postProcessStatusChange] 상태 변경 후 후속 처리 시작 : {$wr_id} old_status = {$old_status} new_status = {$new_status} user_id = {$user_id}");
|
||||
try {
|
||||
// 1. 상태 변경에 따른 알림 발송
|
||||
if (function_exists('notify_by_status')) {
|
||||
$this->write_debug_log_ff('function_exists start');
|
||||
notify_by_status($wr_id, $new_status);
|
||||
}
|
||||
|
||||
// 2. [핵심] '견적채택' 시 후속 처리
|
||||
if ($new_status === '견적채택') {
|
||||
$write = sql_fetch("SELECT wr_num, mb_id FROM {$this->db['write_prefix']}order WHERE wr_id = '{$wr_id}'");
|
||||
if ($write) {
|
||||
$origin_post = sql_fetch("SELECT wr_id FROM {$this->db['write_prefix']}order WHERE wr_num = '{$write['wr_num']}' AND wr_reply = ''");
|
||||
if ($origin_post) {
|
||||
$origin_wr_id = $origin_post['wr_id'];
|
||||
$selected_dealer_id = $write['mb_id'];
|
||||
|
||||
// 2-1. 원본글 상태를 '견적채택'으로 변경
|
||||
$this->updateStatus($origin_wr_id, '계약금입금예정', $user_id);
|
||||
$this->logStatusChange($origin_wr_id, '작성완료', '계약금입금예정', $user_id, "대리점({$selected_dealer_id}) 견적 채택됨");
|
||||
|
||||
// 2-2. 다른 대리점들의 제안을 '견적취소'로 변경
|
||||
$other_replies_sql = "SELECT wr_id FROM {$this->db['write_prefix']}order WHERE wr_num = '{$write['wr_num']}' AND wr_reply != '' AND wr_id != '{$wr_id}'";
|
||||
$other_replies_result = sql_query($other_replies_sql);
|
||||
while ($other_reply = sql_fetch_array($other_replies_result)) {
|
||||
$this->updateStatus($other_reply['wr_id'], '견적취소', 'system');
|
||||
$this->logStatusChange($other_reply['wr_id'], '견적제안', '견적취소', 'system', "다른 견적 채택으로 자동 취소됨");
|
||||
}
|
||||
|
||||
// 2-3. estimate_bidding 테이블의 상태도 업데이트 (선택/거절)
|
||||
$origin_estimate = sql_fetch("SELECT id FROM estimate WHERE wr_id = '{$origin_wr_id}'");
|
||||
if ($origin_estimate) {
|
||||
$estimate_id = $origin_estimate['id'];
|
||||
sql_query("UPDATE estimate_bidding SET status = 'rejected' WHERE estimate_id = '{$estimate_id}'");
|
||||
sql_query("UPDATE estimate_bidding SET status = 'selected' WHERE estimate_id = '{$estimate_id}' AND dealer_id = '{$selected_dealer_id}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("StatusManager::postProcessStatusChange Error: " . $e->getMessage());
|
||||
return ['success' => false, 'message' => '후속 처리 중 오류: ' . $e->getMessage()];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => '후속 처리 완료'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 이 파일은 write_update.php 파일의 가장 마지막, 페이지 이동 직전에 include 됩니다.
|
||||
|
||||
if ($w == '' || $w == 'r') { // 새 글 또는 답변 글일 때만 실행
|
||||
if (isset($wr_id) && $wr_id) {
|
||||
// EstimateManager 클래스 파일 포함
|
||||
include_once(G5_ADMIN_PATH . '/code_manager/classes/EstimateManager.class.php');
|
||||
|
||||
try {
|
||||
$estimateManager = new EstimateManager();
|
||||
// $_POST 데이터를 사용하여 견적서 생성
|
||||
$estimate_id = $estimateManager->createCustomerRequest($wr_id, $_POST);
|
||||
|
||||
if (!$estimate_id) {
|
||||
// 견적서 저장 실패 시 로그를 남기거나 관리자에게 알림을 보낼 수 있습니다.
|
||||
// 예: error_log("견적서 저장 실패: wr_id={$wr_id}");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 예외 발생 시 처리
|
||||
// 예: error_log("견적서 저장 중 예외 발생: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 이 파일은 상담 예약 관련 팝업들을 한번에 쉽게 포함하기 위해 사용됩니다.
|
||||
|
||||
// 현재 파일의 경로를 기준으로 팝업 파일들의 경로를 정의합니다.
|
||||
$expert_visit_components_path = dirname(__FILE__);
|
||||
|
||||
// 상담 예약 신청 팝업 포함
|
||||
include_once($expert_visit_components_path . '/expert_visit_popup.php');
|
||||
|
||||
// 예약 확인/취소 팝업 포함
|
||||
//include_once($expert_visit_components_path . '/my_reservations_popup.php');
|
||||
?>
|
||||
@@ -0,0 +1,207 @@
|
||||
/* 💡 [추가] 로딩 오버레이 스타일 */
|
||||
.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); } }
|
||||
|
||||
/* 팝업 오버레이 */
|
||||
.expert-visit-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;
|
||||
}
|
||||
.expert-visit-modal-overlay.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.expert-visit-modal-content {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
max-width: 550px; /* 너비 약간 축소 */
|
||||
max-height: 85vh; /* 높이 제한 축소 */
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.expert-visit-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px; /* 패딩 축소 */
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.expert-visit-modal-header h2 { margin: 0; font-size: 18px; font-weight: 600; color: #333; }
|
||||
.expert-visit-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;
|
||||
}
|
||||
.expert-visit-modal-close:hover { background: #e9ecef; color: #333; }
|
||||
.expert-visit-modal-body { padding: 20px; } /* 패딩 축소 */
|
||||
|
||||
/* 단계 표시 */
|
||||
.expert-visit-steps { display: flex; justify-content: center; margin-bottom: 20px; position: relative; } /* 마진 축소 */
|
||||
.expert-visit-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: 28px; height: 28px; border-radius: 50%; background: #e9ecef; color: #6c757d; /* 크기 축소 */
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 6px; transition: all 0.3s ease; font-size: 13px;
|
||||
}
|
||||
.step.active .step-number { background: #007bff; color: #fff; }
|
||||
.step.completed .step-number { background: #28a745; color: #fff; }
|
||||
.step-text { font-size: 11px; 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: 15px; } /* 마진 축소 */
|
||||
.calendar-nav {
|
||||
background: none; border: 1px solid #ddd; width: 30px; height: 30px; 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: 16px; 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: 8px 4px; text-align: center; font-weight: 600; color: #495057; font-size: 13px; 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: 13px; position: relative; padding: 2px; /* 폰트 및 패딩 축소 */
|
||||
}
|
||||
.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: 30px 20px; color: #666; font-style: italic; }
|
||||
.error { color: #dc3545; }
|
||||
|
||||
/* 범례 */
|
||||
.calendar-legend, .time-legend { display: flex; justify-content: center; gap: 15px; margin-top: 10px; flex-wrap: wrap; }
|
||||
.legend-item { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #666; }
|
||||
.legend-color { width: 10px; height: 10px; 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: 15px; }
|
||||
.step-description h4 { margin: 0 0 8px 0; font-size: 16px; }
|
||||
.step-description p { margin: 0; font-size: 13px; color: #666; }
|
||||
.selected-date-info h4 { margin: 0; color: #333; font-size: 15px; }
|
||||
.time-slots-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px; margin-bottom: 15px; }
|
||||
.time-slot {
|
||||
padding: 10px 6px; border: 2px solid #e9ecef; border-radius: 6px; text-align: center; cursor: pointer;
|
||||
transition: all 0.2s ease; font-size: 13px; font-weight: 500; display: flex; flex-direction: column; gap: 3px;
|
||||
}
|
||||
.time-text { font-weight: 600; }
|
||||
.slot-info { font-size: 10px; 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; }
|
||||
|
||||
/* 고객 정보 폼 */
|
||||
.expert-visit-summary { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 15px; }
|
||||
.expert-visit-summary h5 { margin: 0 0 8px 0; font-size: 15px; }
|
||||
.summary-grid { display: grid; grid-template-columns: 1fr; gap: 6px; }
|
||||
.summary-item { display: flex; justify-content: space-between; font-size: 13px; }
|
||||
.summary-item .label { font-weight: 500; color: #495057; }
|
||||
.summary-item span { color: #333; }
|
||||
.customer-info-form h4 { margin: 0 0 12px 0; color: #333; font-size: 15px; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.form-group { margin-bottom: 12px; }
|
||||
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: #495057; font-size: 13px; }
|
||||
.required { color: #dc3545; font-weight: bold; }
|
||||
.form-group input, .form-group textarea, .form-group select {
|
||||
width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 6px;
|
||||
font-size: 13px; transition: border-color 0.2s ease; box-sizing: border-box;
|
||||
}
|
||||
.form-group textarea { resize: vertical; min-height: 60px; }
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 네비게이션 버튼 */
|
||||
.expert-visit-nav-buttons {
|
||||
display: flex; justify-content: space-between; gap: 10px; margin-top: 15px;
|
||||
padding-top: 15px; border-top: 1px solid #e9ecef;
|
||||
}
|
||||
.expert-visit-nav-buttons button {
|
||||
padding: 10px 20px; border: none; border-radius: 6px; font-size: 13px;
|
||||
font-weight: 600; cursor: pointer; transition: all 0.2s ease; min-width: 90px;
|
||||
}
|
||||
.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) {
|
||||
.expert-visit-modal-content { margin: 10px; max-height: 95vh; border-radius: 8px; }
|
||||
.expert-visit-modal-header, .expert-visit-modal-body { padding: 15px; }
|
||||
.expert-visit-modal-header h2 { font-size: 16px; }
|
||||
.expert-visit-steps { margin-bottom: 15px; }
|
||||
.step { padding: 0 8px; }
|
||||
.step-number { width: 24px; height: 24px; font-size: 12px; }
|
||||
.step-text { font-size: 10px; }
|
||||
.calendar-day { font-size: 12px; }
|
||||
.time-slots-grid { grid-template-columns: repeat(auto-fit, minmax(80px, 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) {
|
||||
.expert-visit-modal-overlay { padding: 0; align-items: flex-end; }
|
||||
.expert-visit-modal-content { margin: 0; width: 100%; max-height: 90vh; border-radius: 10px 10px 0 0; }
|
||||
.expert-visit-nav-buttons { flex-direction: column-reverse; gap: 10px; }
|
||||
.expert-visit-nav-buttons button { width: 100%; }
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
<?php
|
||||
/**
|
||||
* 전문가 방문 예약 팝업 UI 및 데이터 처리
|
||||
*/
|
||||
|
||||
// AJAX 요청 처리
|
||||
if (isset($_POST['action'])) {
|
||||
include_once('../_common_con.php');
|
||||
include_once('../lib/notification_helper.php');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
|
||||
|
||||
// 월별 예약 가능일 조회
|
||||
if ($action === 'get_expert_visit_month_availability') {
|
||||
$year = (int) ($_POST['year'] ?? 0);
|
||||
$month = (int) ($_POST['month'] ?? 0);
|
||||
$expert_id = $_POST['expert_id'] ?? ''; // 전문가 ID 추가
|
||||
|
||||
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 = get_order_config('expert_visit_max_advance_days', 30);
|
||||
$max_date = date('Y-m-d', strtotime("+" . $max_advance_days . " days"));
|
||||
|
||||
// 날짜별로 루프를 돌며 가용성을 체크합니다.
|
||||
// 쿼리 하나로 처리하기 복잡하므로, 기간 내의 모든 스케줄을 가져와서 PHP에서 병합합니다.
|
||||
|
||||
// 1. 기간 내의 모든 '지정일' 스케줄 가져오기
|
||||
$specific_schedules = [];
|
||||
$sql_specific = "SELECT * FROM expert_visit_schedules
|
||||
WHERE specific_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND (expert_id = '{$expert_id}' OR expert_id IS NULL)
|
||||
ORDER BY expert_id DESC"; // 전문가 설정 우선
|
||||
$res_specific = sql_query($sql_specific);
|
||||
while ($row = sql_fetch_array($res_specific)) {
|
||||
$date = $row['specific_date'];
|
||||
if (!isset($specific_schedules[$date])) { // 이미 전문가 설정이 있으면 공통 설정은 무시
|
||||
$specific_schedules[$date] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. '요일별' 스케줄 가져오기
|
||||
$weekly_schedules = [];
|
||||
$sql_weekly = "SELECT * FROM expert_visit_schedules
|
||||
WHERE day_of_week IS NOT NULL
|
||||
AND (expert_id = '{$expert_id}' OR expert_id IS NULL)
|
||||
ORDER BY expert_id DESC"; // 전문가 설정 우선
|
||||
$res_weekly = sql_query($sql_weekly);
|
||||
while ($row = sql_fetch_array($res_weekly)) {
|
||||
$dow = $row['day_of_week'];
|
||||
if (!isset($weekly_schedules[$dow])) {
|
||||
$weekly_schedules[$dow] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$availability = [];
|
||||
$current = strtotime($start_date);
|
||||
$end = strtotime($end_date);
|
||||
|
||||
while ($current <= $end) {
|
||||
$date_str = date('Y-m-d', $current);
|
||||
$day_num = date('j', $current);
|
||||
$dow = date('N', $current); // 1(월) ~ 7(일)
|
||||
|
||||
// 우선순위: 지정일 > 요일별
|
||||
$schedule = $specific_schedules[$date_str] ?? ($weekly_schedules[$dow] ?? null);
|
||||
|
||||
$is_bookable = false;
|
||||
$reason = '';
|
||||
|
||||
if (!$schedule) {
|
||||
$reason = 'no_schedule'; // 스케줄 없음
|
||||
} elseif ($schedule['is_available'] == 0) {
|
||||
$reason = 'holiday'; // 휴무
|
||||
} elseif ($date_str < date('Y-m-d')) {
|
||||
$reason = 'past_date'; // 지난 날짜
|
||||
} elseif ($date_str > $max_date) {
|
||||
$reason = 'too_far'; // 예약 가능 기간 초과
|
||||
} else {
|
||||
// 예약 꽉 찼는지 확인
|
||||
// 해당 날짜, 해당 전문가(또는 전체)의 예약 건수 확인
|
||||
// 시간대별로 체크해야 정확하지만, 여기서는 '하루 전체 마감' 여부를 대략적으로 판단하거나
|
||||
// 일단 '가능'으로 표시하고 시간 선택에서 막을 수 있습니다.
|
||||
// 정확도를 위해 해당 날짜의 총 슬롯 수와 예약 수를 비교합니다.
|
||||
|
||||
$start_time = strtotime($date_str . ' ' . $schedule['start_time']);
|
||||
$end_time = strtotime($date_str . ' ' . $schedule['end_time']);
|
||||
$slot_duration = $schedule['time_slot'] * 60;
|
||||
$total_slots = 0;
|
||||
for ($t = $start_time; $t < $end_time; $t += $slot_duration) {
|
||||
$total_slots++;
|
||||
}
|
||||
$max_capacity = $total_slots * $schedule['max_persons'];
|
||||
|
||||
// 예약된 건수 조회
|
||||
$sql_reserved = "SELECT COUNT(*) as cnt FROM expert_visit_reservations
|
||||
WHERE visit_date = '{$date_str}' AND status != 'cancelled'";
|
||||
if ($expert_id) {
|
||||
$sql_reserved .= " AND expert_id = '{$expert_id}'";
|
||||
}
|
||||
$row_reserved = sql_fetch($sql_reserved);
|
||||
|
||||
if ($row_reserved['cnt'] >= $max_capacity) {
|
||||
$reason = 'full';
|
||||
} else {
|
||||
$is_bookable = true;
|
||||
}
|
||||
}
|
||||
|
||||
$availability[$day_num] = [
|
||||
'available' => $is_bookable,
|
||||
'reason' => $reason
|
||||
];
|
||||
|
||||
$current = strtotime('+1 day', $current);
|
||||
}
|
||||
|
||||
$response = ['success' => true, 'data' => $availability];
|
||||
}
|
||||
}
|
||||
|
||||
// 특정일의 예약 가능 시간 조회
|
||||
if ($action === 'get_expert_visit_time_slots') {
|
||||
$date = preg_replace('/[^0-9\-]/', '', $_POST['date'] ?? '');
|
||||
$expert_id = $_POST['expert_id'] ?? '';
|
||||
|
||||
if ($date) {
|
||||
$min_advance_hours = get_order_config('expert_visit_min_advance_hours', 24);
|
||||
$min_datetime = date('Y-m-d H:i:s', strtotime("+" . $min_advance_hours . " hours"));
|
||||
$dow = date('N', strtotime($date));
|
||||
|
||||
// 1. 해당 날짜의 스케줄 조회 (우선순위 적용)
|
||||
$schedule = sql_fetch("SELECT * FROM expert_visit_schedules
|
||||
WHERE (specific_date = '{$date}' OR (specific_date IS NULL AND day_of_week = '{$dow}'))
|
||||
AND (expert_id = '{$expert_id}' OR expert_id IS NULL)
|
||||
ORDER BY specific_date DESC, expert_id DESC LIMIT 1");
|
||||
|
||||
$slots = [];
|
||||
if ($schedule && $schedule['is_available']) {
|
||||
$start_time = strtotime($date . ' ' . $schedule['start_time']);
|
||||
$end_time = strtotime($date . ' ' . $schedule['end_time']);
|
||||
$slot_duration = $schedule['time_slot'] * 60;
|
||||
$max_persons = $schedule['max_persons'];
|
||||
|
||||
for ($t = $start_time; $t < $end_time; $t += $slot_duration) {
|
||||
$current_slot_time = date('H:i', $t);
|
||||
$slot_datetime = $date . ' ' . $current_slot_time;
|
||||
|
||||
// 예약 건수 조회
|
||||
$sql_reserved = "SELECT COUNT(*) as cnt FROM expert_visit_reservations
|
||||
WHERE visit_date = '{$date}'
|
||||
AND visit_time = '{$current_slot_time}:00'
|
||||
AND status != 'cancelled'";
|
||||
if ($expert_id) {
|
||||
$sql_reserved .= " AND expert_id = '{$expert_id}'";
|
||||
}
|
||||
$reserved_count = (int)sql_fetch($sql_reserved)['cnt'];
|
||||
|
||||
$is_too_soon = ($slot_datetime < $min_datetime);
|
||||
$is_full = ($reserved_count >= $max_persons);
|
||||
|
||||
$slots[] = [
|
||||
'time' => $current_slot_time,
|
||||
'available' => !$is_too_soon && !$is_full,
|
||||
'reason' => $is_too_soon ? 'too_soon' : ($is_full ? 'full' : ''),
|
||||
'reserved_count' => $reserved_count,
|
||||
'max_persons' => $max_persons
|
||||
];
|
||||
}
|
||||
}
|
||||
$response = ['success' => true, 'data' => $slots];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/order_manage/_common_con.php');
|
||||
include_once(G5_ADMIN_PATH . '/order_manage/lib/notification_helper.php');
|
||||
|
||||
$current_year = date('Y');
|
||||
$current_month_num = date('n');
|
||||
|
||||
$visit_fee = get_order_config('expert_visit_fee', 50000);
|
||||
$account_info = get_order_config('expert_visit_account_info', '');
|
||||
$max_advance_days = get_order_config('expert_visit_max_advance_days', 30);
|
||||
|
||||
$ajax_url = G5_ADMIN_URL . '/order_manage/components/expert_visit_popup.php';
|
||||
$form_action_url = G5_ADMIN_URL . '/order_manage/components/expert_visit_submit.php';
|
||||
$css_url = G5_ADMIN_URL . '/order_manage/components/expert_visit_popup.css';
|
||||
|
||||
$wr_id = isset($_GET['wr_id']) ? (int)$_GET['wr_id'] : 0;
|
||||
|
||||
// 💡 [추가] 로그인 회원 정보 가져오기
|
||||
$customer_name = '';
|
||||
$customer_phone = '';
|
||||
$customer_email = '';
|
||||
|
||||
if (isset($member) && $member['mb_id']) {
|
||||
$customer_name = $member['mb_name'];
|
||||
$customer_phone = $member['mb_hp'];
|
||||
$customer_email = $member['mb_email'];
|
||||
}
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="<?php echo $css_url; ?>">
|
||||
|
||||
<div id="expert-visit-popup-overlay" class="expert-visit-modal-overlay">
|
||||
<div id="expert-visit-popup" class="expert-visit-modal-content">
|
||||
<div class="expert-visit-modal-header">
|
||||
<h2>전문가 방문 예약</h2>
|
||||
<button type="button" class="expert-visit-modal-close" aria-label="팝업 닫기">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="expert-visit-modal-body">
|
||||
<div class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div class="expert-visit-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="expert-visit-form" method="post" action="<?php echo $form_action_url; ?>">
|
||||
<div class="expert-visit-step-content" data-step="1">
|
||||
<div class="step-description">
|
||||
<h4>📅 방문 날짜를 선택해주세요</h4>
|
||||
<p>최대 <?php echo $max_advance_days; ?>일 후까지 예약 가능합니다.</p>
|
||||
<!-- 전문가 선택을 1단계로 이동 -->
|
||||
<div class="form-group" style="margin-top: 15px;">
|
||||
<label for="expert-select-step1">👨⚕️ 전문가 선택</label>
|
||||
<select id="expert-select-step1" name="expert_id_display" class="form-control">
|
||||
<option value="">선택 안 함 (전체 일정 보기)</option>
|
||||
<?php
|
||||
/* 전문가 목록 조회 (실제 DB 연동 필요)
|
||||
$experts = get_experts();
|
||||
foreach ($experts as $expert):
|
||||
?>
|
||||
<option value="<?php echo $expert['id']; ?>"><?php echo htmlspecialchars($expert['name']); ?></option>
|
||||
<?php endforeach;
|
||||
*/
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</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"></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>
|
||||
|
||||
<div class="expert-visit-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"></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>
|
||||
|
||||
<div class="expert-visit-step-content" data-step="3" style="display: none;">
|
||||
<div class="step-description">
|
||||
<h4>📝 고객 정보를 입력해주세요</h4>
|
||||
</div>
|
||||
|
||||
<div class="expert-visit-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($visit_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="홍길동" value="<?php echo htmlspecialchars($customer_name); ?>">
|
||||
</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" value="<?php echo htmlspecialchars($customer_phone); ?>">
|
||||
</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" value="<?php echo htmlspecialchars($customer_email); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="expert-visit-type">🏠 방문 유형</label>
|
||||
<select id="expert-visit-type" name="visit_type">
|
||||
<option value="onsite">현장 방문</option>
|
||||
<option value="online">온라인 상담</option>
|
||||
<option value="phone">전화 상담</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 3단계 전문가 선택은 hidden으로 처리하고 1단계 값과 동기화 -->
|
||||
<input type="hidden" id="expert-visit-resource" name="expert_id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="customer-request">📝 요청사항</label>
|
||||
<textarea id="customer-request" name="request_memo" rows="4" placeholder="방문 관련 요청사항이나 문의사항을 입력해주세요."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="selected-date" name="visit_date">
|
||||
<input type="hidden" id="selected-time" name="visit_time">
|
||||
<input type="hidden" name="payment_amount" value="<?php echo $visit_fee; ?>">
|
||||
<input type="hidden" name="status" value="payment_pending">
|
||||
<input type="hidden" name="wr_id" value="<?php echo $wr_id; ?>">
|
||||
</div>
|
||||
|
||||
<div class="expert-visit-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>
|
||||
|
||||
<script>
|
||||
const ExpertVisitPopup = {
|
||||
elements: {},
|
||||
state: {
|
||||
currentStep: 1,
|
||||
selectedDate: null,
|
||||
selectedTime: null,
|
||||
currentYear: new Date().getFullYear(),
|
||||
currentMonth: new Date().getMonth() + 1,
|
||||
expertId: '', // 전문가 ID 상태 추가
|
||||
},
|
||||
config: {
|
||||
ajaxUrl: '<?php echo $ajax_url; ?>',
|
||||
maxAdvanceDays: <?php echo (int)$max_advance_days; ?>,
|
||||
},
|
||||
|
||||
init() {
|
||||
this.elements.overlay = document.getElementById('expert-visit-popup-overlay');
|
||||
if (!this.elements.overlay) return;
|
||||
|
||||
this.elements.popup = this.elements.overlay.querySelector('.expert-visit-modal-content');
|
||||
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
|
||||
this.elements.form = this.elements.popup.querySelector('#expert-visit-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.expertSelect = this.elements.popup.querySelector('#expert-select-step1');
|
||||
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() {
|
||||
const closeBtn = this.elements.popup.querySelector('.expert-visit-modal-close');
|
||||
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
|
||||
|
||||
this.elements.overlay.addEventListener('click', e => {
|
||||
if (e.target === this.elements.overlay) this.close();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && this.elements.overlay.classList.contains('active')) this.close();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// 전문가 선택 변경 시 달력 갱신
|
||||
if (this.elements.expertSelect) {
|
||||
this.elements.expertSelect.addEventListener('change', (e) => {
|
||||
this.state.expertId = e.target.value;
|
||||
this.state.selectedDate = null; // 날짜 선택 초기화
|
||||
this.renderCalendar();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
open() {
|
||||
this.state.currentYear = new Date().getFullYear();
|
||||
this.state.currentMonth = new Date().getMonth() + 1;
|
||||
this.goToStep(1);
|
||||
this.renderCalendar();
|
||||
this.elements.overlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
close() {
|
||||
this.elements.overlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
this.elements.form.reset();
|
||||
this.state.selectedDate = null;
|
||||
this.state.selectedTime = null;
|
||||
this.state.expertId = '';
|
||||
},
|
||||
|
||||
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('unavailable');
|
||||
} else if (availability && availability.available) {
|
||||
dayElement.classList.add('available');
|
||||
// 이미 선택된 날짜라면 selected 클래스 추가
|
||||
if (this.state.selectedDate === dateStr) {
|
||||
dayElement.classList.add('selected');
|
||||
}
|
||||
dayElement.addEventListener('click', () => this.selectDate(dateStr, dayElement));
|
||||
} else {
|
||||
const reason = availability ? availability.reason : 'unavailable';
|
||||
dayElement.classList.add(reason);
|
||||
}
|
||||
|
||||
if (currentDate.getTime() === today.getTime()) {
|
||||
dayElement.classList.add('today');
|
||||
}
|
||||
|
||||
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_expert_visit_month_availability');
|
||||
formData.append('year', this.state.currentYear);
|
||||
formData.append('month', this.state.currentMonth);
|
||||
formData.append('expert_id', this.state.expertId); // 전문가 ID 전송
|
||||
|
||||
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 = '<div class="loading">시간 정보를 불러오는 중...</div>';
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'get_expert_visit_time_slots');
|
||||
formData.append('date', this.state.selectedDate);
|
||||
formData.append('expert_id', this.state.expertId); // 전문가 ID 전송
|
||||
|
||||
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) {
|
||||
this.elements.timeSlotsGrid.innerHTML = '';
|
||||
if (slots.length === 0) {
|
||||
this.elements.timeSlotsGrid.innerHTML = '<div class="no-slots">해당 날짜에 운영되는 시간이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let hasAvailableSlots = false;
|
||||
slots.forEach(slot => {
|
||||
const slotElement = document.createElement('div');
|
||||
slotElement.className = 'time-slot';
|
||||
|
||||
let slotInfoText = '';
|
||||
if (slot.available) {
|
||||
hasAvailableSlots = true;
|
||||
slotElement.classList.add('available');
|
||||
slotElement.addEventListener('click', () => this.selectTime(slot.time, slotElement));
|
||||
slotInfoText = `예약 ${slot.reserved_count} / ${slot.max_persons}`;
|
||||
} else {
|
||||
slotElement.classList.add(slot.reason || 'full');
|
||||
slotInfoText = slot.reason === 'too_soon' ? '예약 임박' : '마감';
|
||||
}
|
||||
|
||||
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('.expert-visit-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;
|
||||
|
||||
// 전문가 ID 동기화
|
||||
this.elements.form.querySelector('#expert-visit-resource').value = this.state.expertId;
|
||||
},
|
||||
|
||||
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', () => ExpertVisitPopup.init());
|
||||
|
||||
function openExpertVisitPopup(wr_id = 0) {
|
||||
if (wr_id) {
|
||||
const form = ExpertVisitPopup.elements.form;
|
||||
if(form) {
|
||||
let wrIdField = form.querySelector('input[name="wr_id"]');
|
||||
if (!wrIdField) {
|
||||
wrIdField = document.createElement('input');
|
||||
wrIdField.type = 'hidden';
|
||||
wrIdField.name = 'wr_id';
|
||||
form.appendChild(wrIdField);
|
||||
}
|
||||
wrIdField.value = wr_id;
|
||||
}
|
||||
}
|
||||
ExpertVisitPopup.open();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
// 경로를 order_manage에 맞게 수정
|
||||
include_once('../_common_con.php');
|
||||
// 💡 [추가] 알림 헬퍼 포함
|
||||
include_once('../lib/notification_helper.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// 입력 데이터 정리 및 컬럼명 변경
|
||||
$reservation_data = [
|
||||
'wr_id' => (int)($_POST['wr_id'] ?? 0),
|
||||
'customer_name' => trim($_POST['customer_name'] ?? ''),
|
||||
'customer_phone' => trim($_POST['customer_phone'] ?? ''),
|
||||
'customer_email' => trim($_POST['customer_email'] ?? ''),
|
||||
'visit_date' => trim($_POST['visit_date'] ?? ''), // 폼의 name="visit_date"와 일치
|
||||
'visit_time' => trim($_POST['visit_time'] ?? ''), // 폼의 name="visit_time"와 일치
|
||||
'expert_id' => trim($_POST['expert_id'] ?? ''), // [추가] 전문가 ID
|
||||
'temp_2' => trim($_POST['visit_type'] ?? ''), // [추가] 방문 유형 (temp_2에 저장)
|
||||
'request_memo' => trim($_POST['request_memo'] ?? ''), // 폼의 name="request_memo"와 일치
|
||||
'payment_amount' => (int)($_POST['payment_amount'] ?? 0),
|
||||
'status' => 'payment_pending',
|
||||
'created_by' => $member['mb_id'] ?? 'guest'
|
||||
];
|
||||
|
||||
// 필수 항목 유효성 검사
|
||||
if (empty($reservation_data['customer_name']) || empty($reservation_data['customer_phone']) || empty($reservation_data['visit_date']) || empty($reservation_data['visit_time'])) {
|
||||
throw new Exception('필수 예약 정보(이름, 연락처, 날짜, 시간)가 누락되었습니다.');
|
||||
}
|
||||
|
||||
// 예약 가능 여부 재확인 (서버 측 검증)
|
||||
// 1. 해당 날짜/요일에 운영하는 스케줄이 있는지 확인
|
||||
$dow = date('N', strtotime($reservation_data['visit_date']));
|
||||
$expert_condition = $reservation_data['expert_id'] ? "(expert_id = '{$reservation_data['expert_id']}' OR expert_id IS NULL)" : "expert_id IS NULL";
|
||||
|
||||
$sql_schedule = "SELECT * FROM expert_visit_schedules
|
||||
WHERE (specific_date = '{$reservation_data['visit_date']}' OR (specific_date IS NULL AND day_of_week = '{$dow}'))
|
||||
AND {$expert_condition}
|
||||
AND is_available = 1
|
||||
ORDER BY specific_date DESC, expert_id DESC LIMIT 1";
|
||||
$schedule = sql_fetch($sql_schedule);
|
||||
|
||||
if (!$schedule) {
|
||||
throw new Exception('선택하신 날짜는 예약이 불가능합니다.');
|
||||
}
|
||||
|
||||
// 2. 해당 시간대가 운영 시간 내인지 확인
|
||||
$visit_time_ts = strtotime($reservation_data['visit_date'] . ' ' . $reservation_data['visit_time']);
|
||||
$start_time_ts = strtotime($reservation_data['visit_date'] . ' ' . $schedule['start_time']);
|
||||
$end_time_ts = strtotime($reservation_data['visit_date'] . ' ' . $schedule['end_time']);
|
||||
|
||||
if ($visit_time_ts < $start_time_ts || $visit_time_ts >= $end_time_ts) {
|
||||
throw new Exception('선택하신 시간은 운영 시간이 아닙니다.');
|
||||
}
|
||||
|
||||
// 3. 해당 시간대에 예약 정원이 찼는지 확인
|
||||
$sql_reserved = "SELECT COUNT(*) as cnt FROM expert_visit_reservations
|
||||
WHERE visit_date = '{$reservation_data['visit_date']}'
|
||||
AND visit_time = '{$reservation_data['visit_time']}'
|
||||
AND status != 'cancelled'";
|
||||
if ($reservation_data['expert_id']) {
|
||||
$sql_reserved .= " AND expert_id = '{$reservation_data['expert_id']}'";
|
||||
}
|
||||
$reserved = sql_fetch($sql_reserved);
|
||||
|
||||
if ($reserved['cnt'] >= $schedule['max_persons']) {
|
||||
throw new Exception('선택하신 시간은 이미 예약이 마감되었습니다.');
|
||||
}
|
||||
|
||||
// 예약 생성
|
||||
$columns = implode(", ", array_keys($reservation_data));
|
||||
$values = "'" . implode("', '", array_map('sql_real_escape_string', $reservation_data)) . "'";
|
||||
|
||||
$sql = "INSERT INTO expert_visit_reservations ($columns, created_at, updated_at)
|
||||
VALUES ($values, NOW(), NOW())";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
$reservation_id = sql_insert_id();
|
||||
|
||||
// 신규 예약 알림 발송
|
||||
if (function_exists('notify_for_expert_visit')) {
|
||||
notify_for_expert_visit($reservation_id, '신규예약');
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '전문가 방문 예약 신청이 완료되었습니다.']);
|
||||
} else {
|
||||
throw new Exception('데이터베이스 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// 500 오류 방지를 위해 예외 발생 시에도 JSON 응답 반환
|
||||
http_response_code(200);
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
include_once('../../_common.php');
|
||||
// 💡 [추가] 알림 헬퍼 포함
|
||||
include_once('../lib/notification_helper.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!$is_member) {
|
||||
die(json_encode(['success' => false, 'message' => '로그인이 필요합니다.']));
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$reservation_id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
try {
|
||||
if ($action === 'cancel') {
|
||||
if (!$reservation_id) {
|
||||
throw new Exception('예약 ID가 없습니다.');
|
||||
}
|
||||
|
||||
// 본인의 예약인지 확인
|
||||
$sql = "SELECT * FROM expert_visit_reservations WHERE id = '{$reservation_id}' AND (customer_id = '{$member['mb_id']}' OR customer_phone = '{$member['mb_hp']}')";
|
||||
$reservation = sql_fetch($sql);
|
||||
|
||||
if (!$reservation) {
|
||||
throw new Exception('취소할 수 있는 예약 정보가 없거나 권한이 없습니다.');
|
||||
}
|
||||
|
||||
// 이미 취소되었거나 완료된 예약은 변경 불가
|
||||
if ($reservation['status'] === 'cancelled' || $reservation['status'] === 'completed') {
|
||||
throw new Exception('이미 취소되었거나 완료된 예약입니다.');
|
||||
}
|
||||
|
||||
// 예약 취소 처리
|
||||
$sql_update = "UPDATE expert_visit_reservations
|
||||
SET status = 'cancelled',
|
||||
updated_at = NOW(),
|
||||
updated_by = '{$member['mb_id']}'
|
||||
WHERE id = '{$reservation_id}'";
|
||||
|
||||
if (sql_query($sql_update)) {
|
||||
// 💡 [수정] 예약 취소 알림 발송
|
||||
if (function_exists('notify_for_expert_visit')) {
|
||||
notify_for_expert_visit($reservation_id, '예약취소');
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '예약이 정상적으로 취소되었습니다.']);
|
||||
} else {
|
||||
throw new Exception('예약 취소 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Exception('잘못된 요청입니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
// AJAX 요청 처리
|
||||
if (isset($_POST['action'])) {
|
||||
header('Content-Type: application/json');
|
||||
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
include_once('../lib/notification_helper.php');
|
||||
if (!$is_member) {
|
||||
echo json_encode(['success' => false, 'message' => '로그인이 필요합니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$reservation_id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
try {
|
||||
if ($action === 'cancel') {
|
||||
if (!$reservation_id) {
|
||||
throw new Exception('예약 ID가 없습니다.');
|
||||
}
|
||||
|
||||
// 본인의 예약인지 확인
|
||||
$sql = "SELECT * FROM expert_visit_reservations WHERE id = '{$reservation_id}' AND (customer_id = '{$member['mb_id']}' OR customer_phone = '{$member['mb_hp']}')";
|
||||
$reservation = sql_fetch($sql);
|
||||
|
||||
if (!$reservation) {
|
||||
throw new Exception('취소할 수 있는 예약 정보가 없거나 권한이 없습니다.');
|
||||
}
|
||||
|
||||
// 이미 취소되었거나 완료된 예약은 변경 불가
|
||||
if ($reservation['status'] === 'cancelled' || $reservation['status'] === 'completed') {
|
||||
throw new Exception('이미 취소되었거나 완료된 예약입니다.');
|
||||
}
|
||||
|
||||
// 예약 취소 처리
|
||||
$sql_update = "UPDATE expert_visit_reservations
|
||||
SET status = 'cancelled',
|
||||
updated_at = NOW(),
|
||||
updated_by = '{$member['mb_id']}'
|
||||
WHERE id = '{$reservation_id}'";
|
||||
|
||||
if (sql_query($sql_update)) {
|
||||
// 예약 취소 알림 발송
|
||||
if (function_exists('notify_for_expert_visit')) {
|
||||
notify_for_expert_visit($reservation_id, '예약취소');
|
||||
}
|
||||
echo json_encode(['success' => true, 'message' => '예약이 정상적으로 취소되었습니다.']);
|
||||
} else {
|
||||
throw new Exception('예약 취소 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
} elseif ($action === 'get_list') {
|
||||
// 예약 목록 조회
|
||||
$sql = "SELECT *
|
||||
FROM expert_visit_reservations
|
||||
WHERE (customer_id = '{$member['mb_id']}' OR customer_phone = '{$member['mb_hp']}')
|
||||
AND is_deleted = 0
|
||||
ORDER BY visit_date DESC, visit_time DESC";
|
||||
$result = sql_query($sql);
|
||||
$reservations = [];
|
||||
while($row = sql_fetch_array($result)) {
|
||||
$row['status_text'] = get_order_config('reservation_status_' . $row['status'], $row['status']);
|
||||
$row['is_cancellable'] = !in_array($row['status'], ['completed', 'cancelled']);
|
||||
$reservations[] = $row;
|
||||
}
|
||||
echo json_encode(['success' => true, 'data' => $reservations]);
|
||||
} else {
|
||||
throw new Exception('잘못된 요청입니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
$ajax_url = G5_ADMIN_URL . '/order_manage/components/my_reservations_popup.php';
|
||||
include_once(G5_ADMIN_URL . '/order_manage/_common_con.php');
|
||||
include_once(G5_ADMIN_URL . '/order_manage/lib/notification_helper.php');
|
||||
?>
|
||||
|
||||
<!-- 나의 예약 현황 팝업 -->
|
||||
<div id="my-reservations-popup" class="reservation-modal-overlay">
|
||||
<div class="reservation-modal-content">
|
||||
<div class="reservation-modal-header">
|
||||
<h2>나의 예약 현황</h2>
|
||||
<button type="button" class="reservation-modal-close" aria-label="팝업 닫기">×</button>
|
||||
</div>
|
||||
<div class="reservation-modal-body">
|
||||
<div class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div id="reservation-results">
|
||||
<!-- 예약 목록이 여기에 표시됩니다. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 팝업 공통 스타일 (expert_visit_popup.php와 통일) */
|
||||
.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; }
|
||||
|
||||
/* 로딩 스피너 */
|
||||
.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-results { margin-top: 0; }
|
||||
.reservation-item { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; background: #fff; }
|
||||
.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-payment_pending { background: #fff3cd; color: #856404; }
|
||||
.status-reserved { background: #d4edda; color: #155724; }
|
||||
.status-completed { background: #cce5ff; color: #004085; }
|
||||
.status-cancelled { background: #f8d7da; color: #721c24; }
|
||||
|
||||
.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; }
|
||||
.btn-danger { background: #dc3545; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 600; }
|
||||
.btn-danger:hover { background: #c82333; }
|
||||
.cancel-notice { font-size: 13px; color: #666; margin-top: 15px; text-align: right; padding: 10px; background-color: #f8f9fa; border-radius: 4px; }
|
||||
.no-results { text-align: center; color: #666; padding: 40px 0; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const MyReservationsPopup = {
|
||||
elements: {},
|
||||
config: {
|
||||
ajaxUrl: '<?php echo $ajax_url; ?>'
|
||||
},
|
||||
|
||||
init() {
|
||||
this.elements.popup = document.getElementById('my-reservations-popup');
|
||||
if (!this.elements.popup) return;
|
||||
|
||||
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
|
||||
this.elements.resultsContainer = this.elements.popup.querySelector('#reservation-results');
|
||||
|
||||
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.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';
|
||||
this.getReservations(); // 팝업 열릴 때 목록 조회
|
||||
},
|
||||
|
||||
close() {
|
||||
if (!this.elements.popup) return;
|
||||
this.elements.popup.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
},
|
||||
|
||||
async getReservations() {
|
||||
this.showLoading();
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'get_list');
|
||||
|
||||
try {
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
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;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
reservations.forEach(res => {
|
||||
const date = new Date(res.visit_date + ' ' + res.visit_time);
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit' };
|
||||
const formattedDate = date.toLocaleDateString('ko-KR', options);
|
||||
|
||||
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}">${res.status_text}</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">${res.temp_3 || '-'}</span></div>
|
||||
<div class="detail-item"><span class="label">방문 비용:</span><span class="value">${Number(res.payment_amount).toLocaleString()}원</span></div>
|
||||
</div>
|
||||
<div class="reservation-actions">`;
|
||||
|
||||
if (res.is_cancellable) {
|
||||
html += `<button type="button" class="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');
|
||||
formData.append('id', reservationId);
|
||||
|
||||
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.getReservations(); // 목록 새로고침
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("취소 오류:", error);
|
||||
alert('예약 취소 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
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', () => MyReservationsPopup.init());
|
||||
|
||||
function openMyExpertReservationsPopup() {
|
||||
MyReservationsPopup.open();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
$sub_menu = "800300";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '시스템 설정';
|
||||
|
||||
// 설정값 로드
|
||||
$configs = [];
|
||||
$result = sql_query("SELECT * FROM order_config ORDER BY id ASC");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$configs[$row['config_key']] = $row;
|
||||
}
|
||||
|
||||
// POST 요청 처리
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
try {
|
||||
foreach ($_POST['config'] as $key => $value) {
|
||||
$key = clean_xss_tags($key);
|
||||
$value = clean_xss_tags($value);
|
||||
|
||||
if (isset($configs[$key])) {
|
||||
// 기존 설정 업데이트
|
||||
$sql = "UPDATE order_config SET config_value = '" . sql_real_escape_string($value) . "' WHERE config_key = '" . sql_real_escape_string($key) . "'";
|
||||
sql_query($sql);
|
||||
} else {
|
||||
// 새 설정 추가 (보안상 기본적으로는 비활성화, 필요 시 주석 해제)
|
||||
// $sql = "INSERT INTO order_config (config_key, config_value) VALUES ('" . sql_real_escape_string($key) . "', '" . sql_real_escape_string($value) . "')";
|
||||
// sql_query($sql);
|
||||
}
|
||||
}
|
||||
alert('설정이 저장되었습니다.');
|
||||
} catch (Exception $e) {
|
||||
alert('오류: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
견적 관리 솔루션의 주요 설정을 관리합니다.<br>
|
||||
타이머, 수수료율, 알림 모드 등을 설정할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form name="fconfig" method="post">
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?></caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<?php foreach ($configs as $key => $config): ?>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="<?php echo $key; ?>"><?php echo $config['config_desc']; ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<?php if ($config['config_type'] === 'boolean'): ?>
|
||||
<select name="config[<?php echo $key; ?>]" id="<?php echo $key; ?>">
|
||||
<option value="1" <?php echo $config['config_value'] == '1' ? 'selected' : ''; ?>>사용</option>
|
||||
<option value="0" <?php echo $config['config_value'] == '0' ? 'selected' : ''; ?>>미사용</option>
|
||||
</select>
|
||||
<?php elseif ($config['config_type'] === 'select' && $key === 'notification_mode'): ?>
|
||||
<select name="config[<?php echo $key; ?>]" id="<?php echo $key; ?>">
|
||||
<option value="log" <?php echo $config['config_value'] == 'log' ? 'selected' : ''; ?>>개발 모드 (로그만 기록)</option>
|
||||
<option value="send" <?php echo $config['config_value'] == 'send' ? 'selected' : ''; ?>>실제 발송 모드</option>
|
||||
</select>
|
||||
<?php elseif ($config['config_type'] === 'number'): ?>
|
||||
<input type="number" name="config[<?php echo $key; ?>]" id="<?php echo $key; ?>"
|
||||
value="<?php echo htmlspecialchars($config['config_value']); ?>" class="frm_input"
|
||||
style="width: 100px;">
|
||||
<?php if (strpos($config['config_desc'], '%') !== false) echo ' %'; ?>
|
||||
<?php else: ?>
|
||||
<input type="text" name="config[<?php echo $key; ?>]" id="<?php echo $key; ?>"
|
||||
value="<?php echo htmlspecialchars($config['config_value']); ?>" class="frm_input"
|
||||
style="width: 90%;">
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="설정 저장" class="btn_submit">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
$sub_menu = "800300";
|
||||
include_once("./_common.php");
|
||||
|
||||
auth_check_menu($auth, $sub_menu, "w");
|
||||
|
||||
if ($_POST) {
|
||||
$configs = [
|
||||
'timer_enabled',
|
||||
'timer_message_active',
|
||||
'timer_message_inactive',
|
||||
'contract_deposit_rate',
|
||||
'middle_payment_rate',
|
||||
'expert_visit_fee',
|
||||
'expert_account_info'
|
||||
];
|
||||
|
||||
foreach ($configs as $key) {
|
||||
if (isset($_POST[$key])) {
|
||||
$value = sql_real_escape_string($_POST[$key]);
|
||||
$sql = "UPDATE order_config SET config_value = '{$value}', updated_at = NOW() WHERE config_key = '{$key}'";
|
||||
sql_query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
alert('설정이 저장되었습니다.', './config_manager.php');
|
||||
} else {
|
||||
alert('잘못된 접근입니다.', './config_manager.php');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
$sub_menu = "800800";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '상담 예약 관리';
|
||||
|
||||
// lib/notification_helper.php 포함 (설정값 가져오기 위해)
|
||||
if (file_exists(G5_LIB_PATH . '/notification_helper.php')) {
|
||||
include_once(G5_LIB_PATH . '/notification_helper.php');
|
||||
}
|
||||
|
||||
function get_reservation_status_name($status_code) {
|
||||
return get_order_config('reservation_status_' . $status_code, $status_code);
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
$sql_search = "";
|
||||
$sfl = isset($_GET['sfl']) ? clean_xss_tags($_GET['sfl']) : '';
|
||||
$stx = isset($_GET['stx']) ? clean_xss_tags($_GET['stx']) : '';
|
||||
$sst = isset($_GET['sst']) ? clean_xss_tags($_GET['sst']) : 'id';
|
||||
$sod = isset($_GET['sod']) ? clean_xss_tags($_GET['sod']) : 'desc';
|
||||
$status_filter = isset($_GET['status_filter']) ? clean_xss_tags($_GET['status_filter']) : '';
|
||||
|
||||
if ($stx) {
|
||||
$stx_escaped = sql_real_escape_string($stx);
|
||||
if ($sfl === 'customer_name') {
|
||||
$sql_search .= " AND customer_name LIKE '%{$stx_escaped}%' ";
|
||||
} elseif ($sfl === 'customer_phone') {
|
||||
$sql_search .= " AND customer_phone LIKE '%{$stx_escaped}%' ";
|
||||
}
|
||||
}
|
||||
|
||||
if ($status_filter) {
|
||||
$sql_search .= " AND status = '" . sql_real_escape_string($status_filter) . "' ";
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sql_order = " ORDER BY {$sst} {$sod} ";
|
||||
|
||||
// 페이징
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$page_rows = 15;
|
||||
$from_record = ($page - 1) * $page_rows;
|
||||
|
||||
// 전체 개수
|
||||
$sql_count = "SELECT COUNT(*) as cnt FROM consultant_reservations WHERE is_deleted = 0 {$sql_search}";
|
||||
$count_row = sql_fetch($sql_count);
|
||||
$total_count = $count_row['cnt'];
|
||||
$total_page = ceil($total_count / $page_rows);
|
||||
|
||||
// 데이터 조회
|
||||
$sql = "SELECT * FROM consultant_reservations WHERE is_deleted = 0 {$sql_search} {$sql_order} LIMIT {$from_record}, {$page_rows}";
|
||||
$result = sql_query($sql);
|
||||
$reservations = [];
|
||||
while($row = sql_fetch_array($result)) {
|
||||
$reservations[] = $row;
|
||||
}
|
||||
|
||||
// 상담가 목록 조회 (예: 레벨 8)
|
||||
$consultants_result = sql_query("SELECT mb_id, mb_name FROM {$g5['member_table']} WHERE mb_level = 8 AND mb_leave_date = '' ORDER BY mb_name ASC");
|
||||
$consultants = [];
|
||||
while($row = sql_fetch_array($consultants_result)) {
|
||||
$consultants[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
$qstr = http_build_query([
|
||||
'sfl' => $sfl,
|
||||
'stx' => $stx,
|
||||
'sst' => $sst,
|
||||
'sod' => $sod,
|
||||
'status_filter' => $status_filter
|
||||
]);
|
||||
?>
|
||||
|
||||
<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>
|
||||
<?php echo "DDDDD => ".$sql_count ?>
|
||||
<?php echo "DDDDD=> ".$sql ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<label for="status_filter" class="sound_only">상태 필터</label>
|
||||
<select name="status_filter" id="status_filter">
|
||||
<option value="">상태 전체</option>
|
||||
<option value="payment_pending" <?php echo get_selected($status_filter, 'payment_pending'); ?>><?php echo get_reservation_status_name('payment_pending'); ?></option>
|
||||
<option value="reserved" <?php echo get_selected($status_filter, 'reserved'); ?>><?php echo get_reservation_status_name('reserved'); ?></option>
|
||||
<option value="completed" <?php echo get_selected($status_filter, 'completed'); ?>><?php echo get_reservation_status_name('completed'); ?></option>
|
||||
<option value="cancelled" <?php echo get_selected($status_filter, 'cancelled'); ?>><?php echo get_reservation_status_name('cancelled'); ?></option>
|
||||
</select>
|
||||
|
||||
<label for="sfl" class="sound_only">검색대상</label>
|
||||
<select name="sfl" id="sfl">
|
||||
<option value="customer_name" <?php echo get_selected($sfl, 'customer_name'); ?>>고객명</option>
|
||||
<option value="customer_phone" <?php echo get_selected($sfl, 'customer_phone'); ?>>연락처</option>
|
||||
</select>
|
||||
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" class="frm_input">
|
||||
<input type="submit" class="btn_submit" value="검색">
|
||||
</form>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">예약번호</th>
|
||||
<th scope="col">고객명</th>
|
||||
<th scope="col">연락처</th>
|
||||
<th scope="col">예약일시</th>
|
||||
<th scope="col">상담종류</th>
|
||||
<th scope="col">상태</th>
|
||||
<th scope="col">담당상담가</th>
|
||||
<th scope="col">신청일</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($reservations)): ?>
|
||||
<tr><td colspan="9" class="empty_table">자료가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($reservations as $res): ?>
|
||||
<tr>
|
||||
<td><?php echo $res['id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($res['customer_name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($res['customer_phone']); ?></td>
|
||||
<td><?php echo $res['reservation_date'] . ' ' . substr($res['reservation_time'], 0, 5); ?></td>
|
||||
<td><?php echo htmlspecialchars($res['temp_2']); ?></td>
|
||||
<td><?php echo get_reservation_status_name($res['status']); ?></td>
|
||||
<td><?php echo $res['consultant_id'] ? (get_member($res['consultant_id'])['mb_name'] ?? $res['consultant_id']) : '미배정'; ?></td>
|
||||
<td><?php echo substr($res['created_at'], 0, 10); ?></td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<button type="button" class="btn btn_03 manage-btn" data-id="<?php echo $res['id']; ?>">상세/관리</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'].'?'.$qstr.'&page='); ?>
|
||||
|
||||
<!-- 상세/관리 모달 -->
|
||||
<div id="reservationModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:1000;">
|
||||
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); background:white; padding:20px; border-radius:5px; width:600px;">
|
||||
<h3 id="modalTitle">상담 예약 상세/관리</h3>
|
||||
<form name="fmodal" id="fmodal">
|
||||
<input type="hidden" name="id" id="modal_id">
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">고객 정보</th>
|
||||
<td id="modal_customer_info"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">예약 정보</th>
|
||||
<td id="modal_reservation_info"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">상담 정보</th>
|
||||
<td id="modal_consult_info"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">고객 요청사항</th>
|
||||
<td><textarea id="modal_request_memo" readonly style="width:100%;height:60px;"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_status">상태 변경</label></th>
|
||||
<td>
|
||||
<select name="status" id="modal_status">
|
||||
<option value="payment_pending"><?php echo get_reservation_status_name('payment_pending'); ?></option>
|
||||
<option value="reserved"><?php echo get_reservation_status_name('reserved'); ?></option>
|
||||
<option value="completed"><?php echo get_reservation_status_name('completed'); ?></option>
|
||||
<option value="cancelled"><?php echo get_reservation_status_name('cancelled'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_consultant_id">상담가 배정</label></th>
|
||||
<td>
|
||||
<select name="consultant_id" id="modal_consultant_id">
|
||||
<option value="">상담가 선택</option>
|
||||
<?php foreach ($consultants as $c): ?>
|
||||
<option value="<?php echo $c['mb_id']; ?>"><?php echo htmlspecialchars($c['mb_name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_admin_memo">관리자 메모</label></th>
|
||||
<td><textarea name="admin_memo" id="modal_admin_memo" style="width:100%;height:80px;"></textarea></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn_confirm01 btn_confirm" style="margin-top:20px;">
|
||||
<button type="button" id="modalSaveBtn" class="btn_submit">저장</button>
|
||||
<button type="button" id="modalCloseBtn" class="btn_cancel">닫기</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = document.getElementById('reservationModal');
|
||||
const manageButtons = document.querySelectorAll('.manage-btn');
|
||||
const closeBtn = document.getElementById('modalCloseBtn');
|
||||
const saveBtn = document.getElementById('modalSaveBtn');
|
||||
|
||||
function openModal(id) {
|
||||
fetch('./consultant_reservations_ajax.php?action=get_details&id=' + id)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const res = data.data;
|
||||
document.getElementById('modal_id').value = res.id;
|
||||
document.getElementById('modalTitle').innerText = `상담 예약 상세/관리 (예약번호: ${res.id})`;
|
||||
document.getElementById('modal_customer_info').innerHTML = `${res.customer_name} (${res.customer_phone} / ${res.customer_email})`;
|
||||
document.getElementById('modal_reservation_info').innerHTML = `${res.reservation_date} ${res.reservation_time.substring(0,5)} / <strong>상담비: ${Number(res.payment_amount).toLocaleString()}원</strong>`;
|
||||
document.getElementById('modal_consult_info').innerHTML = `종류: ${res.temp_2} / 지역: ${res.temp_3} / 예상시간: ${res.temp_4}분`;
|
||||
document.getElementById('modal_request_memo').value = res.request_memo;
|
||||
document.getElementById('modal_status').value = res.status;
|
||||
document.getElementById('modal_consultant_id').value = res.consultant_id || '';
|
||||
document.getElementById('modal_admin_memo').value = res.admin_memo;
|
||||
modal.style.display = 'block';
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
manageButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
openModal(this.dataset.id);
|
||||
});
|
||||
});
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', function() {
|
||||
const form = document.getElementById('fmodal');
|
||||
const formData = new FormData(form);
|
||||
formData.append('action', 'update_reservation');
|
||||
|
||||
if (!confirm('변경 내용을 저장하시겠습니까?')) return;
|
||||
|
||||
fetch('./consultant_reservations_ajax.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams(formData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('오류: ' + data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
die(json_encode(['success' => false, 'message' => '관리자 권한이 필요합니다.']));
|
||||
}
|
||||
|
||||
// lib/notification_helper.php 포함
|
||||
if (file_exists(G5_LIB_PATH . '/notification_helper.php')) {
|
||||
include_once(G5_LIB_PATH . '/notification_helper.php');
|
||||
}
|
||||
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
$id = isset($_REQUEST['id']) ? (int)$_REQUEST['id'] : 0;
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'get_details':
|
||||
if (!$id) throw new Exception('예약 ID가 없습니다.');
|
||||
|
||||
$reservation = sql_fetch("SELECT * FROM consultant_reservations WHERE id = '{$id}'");
|
||||
if (!$reservation) throw new Exception('예약 정보를 찾을 수 없습니다.');
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $reservation]);
|
||||
break;
|
||||
|
||||
case 'update_reservation':
|
||||
if (!$id) throw new Exception('예약 ID가 없습니다.');
|
||||
|
||||
$status = isset($_POST['status']) ? clean_xss_tags($_POST['status']) : '';
|
||||
$consultant_id = isset($_POST['consultant_id']) ? clean_xss_tags($_POST['consultant_id']) : '';
|
||||
$admin_memo = isset($_POST['admin_memo']) ? clean_xss_tags($_POST['admin_memo']) : '';
|
||||
|
||||
// 기존 정보 조회 (상태 변경 시 알림을 위함)
|
||||
$old_reservation = sql_fetch("SELECT status FROM consultant_reservations WHERE id = '{$id}'");
|
||||
|
||||
$sql = "UPDATE consultant_reservations SET " .
|
||||
" status = '" . sql_real_escape_string($status) . "', " .
|
||||
" consultant_id = '" . sql_real_escape_string($consultant_id) . "', " .
|
||||
" admin_memo = '" . sql_real_escape_string($admin_memo) . "', " .
|
||||
" updated_at = NOW(), " .
|
||||
" updated_by = '{$member['mb_id']}' " .
|
||||
" WHERE id = '{$id}' ";
|
||||
sql_query($sql);
|
||||
|
||||
// 상태 변경 시 알림 발송
|
||||
if (function_exists('notify_for_consulting')) {
|
||||
// 입금 확인 -> 예약 확정
|
||||
if ($old_reservation['status'] !== 'reserved' && $status === 'reserved') {
|
||||
notify_for_consulting($id, '예약확정');
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '예약 정보가 성공적으로 업데이트되었습니다.']);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('유효하지 않은 요청입니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
// 중도금 알림 크론 작업 스크립트
|
||||
// 매일 실행하여 시공 2일 전 고객들에게 중도금 알림 발송
|
||||
|
||||
include_once('../../common.php');
|
||||
include_once(G5_LIB_PATH . '/notification_helper.php');
|
||||
|
||||
// CLI에서만 실행 가능하도록 제한
|
||||
if (php_sapi_name() !== 'cli' && !$is_admin) {
|
||||
die('Access denied');
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
|
||||
// 오늘이 중도금 알림일인 견적들 조회
|
||||
$sql = "SELECT e.*, w.wr_name, w.wr_subject, w.mb_hp,
|
||||
(SELECT mb_name FROM g5_member WHERE mb_id = e.extra_4) as dealer_name
|
||||
FROM estimate e
|
||||
JOIN g5_write_order w ON e.wr_id = w.wr_id
|
||||
WHERE e.extra_5 = '{$today}'
|
||||
AND e.status = '입금확인'
|
||||
AND e.is_deleted = 0";
|
||||
|
||||
$result = sql_query($sql);
|
||||
|
||||
$processed_count = 0;
|
||||
$middle_payment_rate = (int) get_order_config('middle_payment_rate', '40');
|
||||
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
// 견적 총액 조회
|
||||
$total_amount_result = sql_fetch("SELECT SUM(ei.amount) as total_amount
|
||||
FROM estimate_item ei
|
||||
WHERE ei.estimate_id = '{$row['id']}'
|
||||
AND ei.dealer_id = '{$row['extra_4']}'");
|
||||
|
||||
$total_amount = $total_amount_result['total_amount'] ?? 0;
|
||||
$interim_amount = ($total_amount * $middle_payment_rate) / 100;
|
||||
|
||||
// 중도금 알림 발송
|
||||
$notification_result = notify_customer('customer_interim_payment', [
|
||||
'customer_name' => $row['wr_name'],
|
||||
'interim_amount' => number_format($interim_amount),
|
||||
'construction_date' => $row['temp_4'],
|
||||
'dealer_name' => $row['dealer_name']
|
||||
]);
|
||||
|
||||
if ($notification_result) {
|
||||
// 상태를 중도금입금예정으로 변경
|
||||
sql_query("UPDATE estimate SET
|
||||
status = '중도금입금예정',
|
||||
updated_at = NOW()
|
||||
WHERE id = '{$row['id']}'");
|
||||
|
||||
// 게시판 상태도 업데이트
|
||||
sql_query("UPDATE g5_write_order SET wr_1 = '중도금입금예정' WHERE wr_id = '{$row['wr_id']}'");
|
||||
|
||||
$processed_count++;
|
||||
|
||||
// 로그 기록
|
||||
error_log("[CRON] 중도금 알림 발송 완료 - wr_id: {$row['wr_id']}, 고객: {$row['wr_name']}, 금액: " . number_format($interim_amount));
|
||||
}
|
||||
}
|
||||
|
||||
echo "중도금 알림 처리 완료: {$processed_count}건\n";
|
||||
|
||||
// 템플릿이 없는 경우를 위한 기본 템플릿 추가
|
||||
$template_check = sql_fetch("SELECT COUNT(*) as cnt FROM order_mail_templates WHERE template_key = 'customer_interim_payment'");
|
||||
if ($template_check['cnt'] == 0) {
|
||||
// 중도금 알림 템플릿 추가
|
||||
sql_query("INSERT INTO order_mail_templates (template_key, template_name, subject, content, variables) VALUES
|
||||
('customer_interim_payment', '고객 - 중도금 입금 안내', '시공 준비를 위한 중도금 입금 안내',
|
||||
'안녕하세요 {customer_name}님,<br><br>{construction_date} 시공 예정으로 중도금 입금을 안내드립니다.<br><br>중도금: {interim_amount}원<br>시공업체: {dealer_name}<br><br>입금 확인 후 시공이 진행됩니다.',
|
||||
'[\"customer_name\", \"interim_amount\", \"construction_date\", \"dealer_name\"]')");
|
||||
|
||||
sql_query("INSERT INTO order_sms_templates (template_key, template_name, content, variables) VALUES
|
||||
('customer_interim_payment', '고객 - 중도금 입금 안내',
|
||||
'{customer_name}님, {construction_date} 시공 예정으로 중도금 {interim_amount}원 입금 안내드립니다.',
|
||||
'[\"customer_name\", \"interim_amount\", \"construction_date\"]')");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* 중도금 입금 알림 자동 발송 크론 작업
|
||||
*
|
||||
* 매일 실행하여 중도금 입금 기한이 도래한 견적에 대해 대리점에게 알림을 발송합니다.
|
||||
*
|
||||
* 사용법:
|
||||
* 1. 웹 브라우저: http://yourdomain.com/adm/order_manage/cron_interim_reminders.php?key=your_secret_key
|
||||
* 2. 커맨드라인: php /path/to/adm/order_manage/cron_interim_reminders.php
|
||||
* 3. 크론탭: 0 9 * * * php /path/to/adm/order_manage/cron_interim_reminders.php
|
||||
*/
|
||||
|
||||
// 보안 키 확인 (웹 접근 시)
|
||||
$security_key = 'order_cron_2024';
|
||||
if (isset($_GET['key']) && $_GET['key'] !== $security_key) {
|
||||
die('Invalid security key');
|
||||
}
|
||||
|
||||
// 그누보드 환경 로드
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
$g5_path = dirname(dirname(dirname(__FILE__)));
|
||||
include_once($g5_path . '/common.php');
|
||||
}
|
||||
|
||||
// 로그 함수
|
||||
function log_message($message)
|
||||
{
|
||||
$log_file = G5_DATA_PATH . '/log/interim_reminders.log';
|
||||
$log_dir = dirname($log_file);
|
||||
|
||||
if (!is_dir($log_dir)) {
|
||||
@mkdir($log_dir, 0755, true);
|
||||
}
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$log_entry = "[{$timestamp}] {$message}" . PHP_EOL;
|
||||
|
||||
file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
|
||||
|
||||
// 콘솔 출력 (CLI 모드)
|
||||
if (php_sapi_name() === 'cli') {
|
||||
echo $log_entry;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
log_message("중도금 알림 크론 작업 시작");
|
||||
|
||||
// DealerNotification 클래스 로드
|
||||
require_once G5_PATH . '/adm/order_manage/dealer_notification.php';
|
||||
$dealer_notification = new DealerNotification();
|
||||
|
||||
// 중도금 입금 알림 일괄 발송
|
||||
$result = $dealer_notification->sendInterimPaymentReminders();
|
||||
|
||||
if ($result['success']) {
|
||||
log_message("알림 발송 완료: " . $result['message']);
|
||||
log_message("발송 건수: " . $result['sent_count']);
|
||||
|
||||
// 상세 결과 로그
|
||||
foreach ($result['results'] as $item) {
|
||||
$status = $item['result']['success'] ? 'SUCCESS' : 'FAILED';
|
||||
$message = $item['result']['message'];
|
||||
log_message("견적 {$item['wr_id']} (대리점: {$item['dealer_id']}) - {$status}: {$message}");
|
||||
}
|
||||
} else {
|
||||
log_message("알림 발송 실패: " . $result['message']);
|
||||
}
|
||||
|
||||
// 추가 작업: 오래된 로그 파일 정리 (30일 이상)
|
||||
$log_files = glob(G5_DATA_PATH . '/log/interim_reminders_*.log');
|
||||
$cutoff_date = strtotime('-30 days');
|
||||
|
||||
foreach ($log_files as $log_file) {
|
||||
if (filemtime($log_file) < $cutoff_date) {
|
||||
@unlink($log_file);
|
||||
log_message("오래된 로그 파일 삭제: " . basename($log_file));
|
||||
}
|
||||
}
|
||||
|
||||
log_message("중도금 알림 크론 작업 완료");
|
||||
|
||||
// 웹 접근 시 JSON 응답
|
||||
if (isset($_GET['key'])) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '중도금 알림 발송이 완료되었습니다.',
|
||||
'sent_count' => $result['sent_count'] ?? 0,
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$error_message = "크론 작업 오류: " . $e->getMessage();
|
||||
log_message($error_message);
|
||||
|
||||
// 웹 접근 시 에러 응답
|
||||
if (isset($_GET['key'])) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $error_message,
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
// CLI 모드에서는 exit code 1로 종료
|
||||
if (php_sapi_name() === 'cli') {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
$sub_menu = "800250";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '대리점 설정 관리';
|
||||
|
||||
// 액션 처리
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
|
||||
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
try {
|
||||
if ($action === 'update_commission') {
|
||||
$level_5_rate = (float) ($_POST['dealer_commission_level_5'] ?? 5);
|
||||
$level_6_rate = (float) ($_POST['dealer_commission_level_6'] ?? 11);
|
||||
$level_7_rate = (float) ($_POST['dealer_commission_level_7'] ?? 19);
|
||||
|
||||
// 수수료율 업데이트
|
||||
$updates = [
|
||||
'dealer_commission_level_5' => $level_5_rate,
|
||||
'dealer_commission_level_6' => $level_6_rate,
|
||||
'dealer_commission_level_7' => $level_7_rate
|
||||
];
|
||||
|
||||
foreach ($updates as $key => $value) {
|
||||
$existing = sql_fetch("SELECT config_key FROM order_config WHERE config_key = '{$key}'");
|
||||
if ($existing) {
|
||||
sql_query("UPDATE order_config SET config_value = '{$value}' WHERE config_key = '{$key}'");
|
||||
} else {
|
||||
sql_query("INSERT INTO order_config (config_key, config_value, description, type) VALUES ('{$key}', '{$value}', '대리점 수수료율', 'number')");
|
||||
}
|
||||
}
|
||||
|
||||
alert('대리점 수수료율이 업데이트되었습니다.', './dealer_config.php');
|
||||
}
|
||||
|
||||
if ($action === 'update_dealer_level') {
|
||||
$mb_id = clean_xss_tags($_POST['mb_id'] ?? '');
|
||||
$new_level = (int) ($_POST['new_level'] ?? 0);
|
||||
|
||||
if ($mb_id && in_array($new_level, [5, 6, 7, 8])) {
|
||||
sql_query("UPDATE g5_member SET mb_level = '{$new_level}' WHERE mb_id = '{$mb_id}'");
|
||||
alert('대리점 레벨이 변경되었습니다.', './dealer_config.php');
|
||||
} else {
|
||||
throw new Exception('유효하지 않은 레벨입니다.');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert('오류: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
goto_url('./dealer_config.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 현재 수수료율 조회
|
||||
$commission_rates = [];
|
||||
$rates_sql = "SELECT config_key, config_value FROM order_config WHERE config_key LIKE 'dealer_commission_level_%'";
|
||||
$rates_result = sql_query($rates_sql);
|
||||
while ($row = sql_fetch_array($rates_result)) {
|
||||
$commission_rates[$row['config_key']] = $row['config_value'];
|
||||
}
|
||||
|
||||
// 대리점 목록 조회 (레벨 5-7)
|
||||
$dealers = [];
|
||||
$dealers_sql = "SELECT mb_id, mb_name, mb_level, mb_hp, mb_email, mb_datetime FROM g5_member WHERE mb_level IN (5,6,7) ORDER BY mb_level DESC, mb_name ASC";
|
||||
$dealers_result = sql_query($dealers_sql);
|
||||
while ($dealer = sql_fetch_array($dealers_result)) {
|
||||
$dealers[] = $dealer;
|
||||
}
|
||||
|
||||
// 대리점 성과 조회
|
||||
$performance = [];
|
||||
$performance_sql = "
|
||||
SELECT
|
||||
b.dealer_id,
|
||||
COUNT(*) as total_bids,
|
||||
COUNT(CASE WHEN b.status = 'selected' THEN 1 END) as selected_bids,
|
||||
AVG(b.total_amount) as avg_amount,
|
||||
SUM(CASE WHEN b.status = 'selected' THEN b.total_amount ELSE 0 END) as total_revenue
|
||||
FROM estimate_bidding b
|
||||
WHERE b.created_at >= DATE_SUB(NOW(), INTERVAL 3 MONTH)
|
||||
GROUP BY b.dealer_id
|
||||
";
|
||||
$performance_result = sql_query($performance_sql);
|
||||
while ($perf = sql_fetch_array($performance_result)) {
|
||||
$performance[$perf['dealer_id']] = $perf;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
대리점 수수료율 설정 및 대리점 레벨 관리를 할 수 있습니다.<br>
|
||||
레벨별 수수료율은 견적 제안 시 자동으로 적용됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 수수료율 설정 -->
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<form name="fcommission" method="post">
|
||||
<input type="hidden" name="action" value="update_commission">
|
||||
|
||||
<table>
|
||||
<caption>대리점 수수료율 설정</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="dealer_commission_level_5">레벨 5 대리점 수수료율</label></th>
|
||||
<td>
|
||||
<input type="number" name="dealer_commission_level_5" id="dealer_commission_level_5"
|
||||
value="<?php echo $commission_rates['dealer_commission_level_5'] ?? 5; ?>" class="frm_input"
|
||||
min="0" max="50" step="0.1"> %
|
||||
<span class="frm_info">기본 대리점 수수료율</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="dealer_commission_level_6">레벨 6 대리점 수수료율</label></th>
|
||||
<td>
|
||||
<input type="number" name="dealer_commission_level_6" id="dealer_commission_level_6"
|
||||
value="<?php echo $commission_rates['dealer_commission_level_6'] ?? 11; ?>"
|
||||
class="frm_input" min="0" max="50" step="0.1"> %
|
||||
<span class="frm_info">중급 대리점 수수료율</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="dealer_commission_level_7">레벨 7 대리점 수수료율</label></th>
|
||||
<td>
|
||||
<input type="number" name="dealer_commission_level_7" id="dealer_commission_level_7"
|
||||
value="<?php echo $commission_rates['dealer_commission_level_7'] ?? 19; ?>"
|
||||
class="frm_input" min="0" max="50" step="0.1"> %
|
||||
<span class="frm_info">고급 대리점 수수료율</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="수수료율 저장" class="btn_submit">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 대리점 목록 및 관리 -->
|
||||
<div class="local_ov01 local_ov">
|
||||
<span class="btn_ov01">
|
||||
<span class="ov_txt">등록된 대리점 </span>
|
||||
<span class="ov_num"><?php echo count($dealers); ?>개</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>대리점 목록 및 성과</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">대리점 정보</th>
|
||||
<th scope="col">레벨</th>
|
||||
<th scope="col">수수료율</th>
|
||||
<th scope="col">최근 3개월 성과</th>
|
||||
<th scope="col">가입일</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($dealers)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="empty_table">등록된 대리점이 없습니다.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($dealers as $dealer): ?>
|
||||
<?php
|
||||
$perf = $performance[$dealer['mb_id']] ?? null;
|
||||
$success_rate = $perf && $perf['total_bids'] > 0 ? round(($perf['selected_bids'] / $perf['total_bids']) * 100, 1) : 0;
|
||||
$commission_key = 'dealer_commission_level_' . $dealer['mb_level'];
|
||||
$commission_rate = $commission_rates[$commission_key] ?? 0;
|
||||
?>
|
||||
<tr>
|
||||
<td class="td_left">
|
||||
<strong><?php echo get_text($dealer['mb_name']); ?></strong><br>
|
||||
<small>ID: <?php echo $dealer['mb_id']; ?></small><br>
|
||||
<small>연락처: <?php echo get_text($dealer['mb_hp']); ?></small><br>
|
||||
<small>이메일: <?php echo get_text($dealer['mb_email']); ?></small>
|
||||
</td>
|
||||
<td class="td_center">
|
||||
<span class="level-badge level-<?php echo $dealer['mb_level']; ?>">
|
||||
레벨 <?php echo $dealer['mb_level']; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="td_center">
|
||||
<strong><?php echo $commission_rate; ?>%</strong>
|
||||
</td>
|
||||
<td class="td_left">
|
||||
<?php if ($perf): ?>
|
||||
<strong>입찰: <?php echo number_format($perf['total_bids']); ?>건</strong><br>
|
||||
<small>선택: <?php echo number_format($perf['selected_bids']); ?>건
|
||||
(<?php echo $success_rate; ?>%)</small><br>
|
||||
<small>매출: <?php echo number_format($perf['total_revenue']); ?>원</small><br>
|
||||
<small>평균: <?php echo number_format($perf['avg_amount']); ?>원</small>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">성과 데이터 없음</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="td_datetime">
|
||||
<?php echo date('Y-m-d', strtotime($dealer['mb_datetime'])); ?>
|
||||
</td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<button type="button" class="btn btn_03"
|
||||
onclick="changeDealerLevel('<?php echo $dealer['mb_id']; ?>', <?php echo $dealer['mb_level']; ?>)">
|
||||
레벨 변경
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 레벨 변경 모달 -->
|
||||
<div id="levelModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>대리점 레벨 변경</h3>
|
||||
<span class="close" onclick="closeModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="levelForm" method="post">
|
||||
<input type="hidden" name="action" value="update_dealer_level">
|
||||
<input type="hidden" id="modal_mb_id" name="mb_id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_level">새 레벨:</label>
|
||||
<select id="new_level" name="new_level" class="frm_input" required>
|
||||
<option value="5">레벨 5 (기본 대리점 -
|
||||
<?php echo $commission_rates['dealer_commission_level_5'] ?? 5; ?>%)</option>
|
||||
<option value="6">레벨 6 (중급 대리점 -
|
||||
<?php echo $commission_rates['dealer_commission_level_6'] ?? 11; ?>%)</option>
|
||||
<option value="7">레벨 7 (고급 대리점 -
|
||||
<?php echo $commission_rates['dealer_commission_level_7'] ?? 19; ?>%)</option>
|
||||
<option value="8">레벨 8 (관리자)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="레벨 변경" class="btn_submit">
|
||||
<button type="button" onclick="closeModal()" class="btn_cancel">취소</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.level-badge {
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.level-5 {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.level-6 {
|
||||
background-color: #007bff;
|
||||
}
|
||||
|
||||
.level-7 {
|
||||
background-color: #6f42c1;
|
||||
}
|
||||
|
||||
.modal {
|
||||
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: #fefefe;
|
||||
margin: 15% auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 400px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 15px 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 대리점 레벨 변경
|
||||
function changeDealerLevel(mb_id, current_level) {
|
||||
document.getElementById('modal_mb_id').value = mb_id;
|
||||
document.getElementById('new_level').value = current_level;
|
||||
document.getElementById('levelModal').style.display = 'block';
|
||||
}
|
||||
|
||||
// 모달 닫기
|
||||
function closeModal() {
|
||||
document.getElementById('levelModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById('levelModal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// 필요한 클래스 및 라이브러리 포함
|
||||
require_once G5_PATH . '/adm/order_manage/classes/EstimateManager.class.php';
|
||||
require_once G5_PATH . '/lib/notification_helper.php';
|
||||
|
||||
/**
|
||||
* Class DealerNotification
|
||||
* 대리점 관련 알림을 처리하는 클래스
|
||||
*/
|
||||
class DealerNotification
|
||||
{
|
||||
private $estimateManager;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->estimateManager = new EstimateManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 대리점의 견적이 선택되었을 때 알림을 발송합니다.
|
||||
*
|
||||
* @param int $wr_id 게시물 ID
|
||||
* @param string $dealer_id 대리점 ID
|
||||
* @param array $bid_info 입찰 정보
|
||||
* @return array ['success' => bool, 'message' => string]
|
||||
*/
|
||||
public function sendBidSelectedNotification($wr_id, $dealer_id, $bid_info)
|
||||
{
|
||||
try {
|
||||
// 1. 고객 정보 조회
|
||||
$write = sql_fetch("SELECT * FROM g5_write_order WHERE wr_id = '{$wr_id}'");
|
||||
if (!$write) {
|
||||
throw new Exception('원본 게시물을 찾을 수 없습니다.');
|
||||
}
|
||||
$customer = get_member($write['mb_id']);
|
||||
|
||||
// 2. 대리점 정보 조회
|
||||
$dealer = get_member($dealer_id);
|
||||
if (!$dealer) {
|
||||
throw new Exception('대리점 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 3. 알림에 필요한 변수 설정
|
||||
$vars = [
|
||||
'dealer_name' => $dealer['mb_name'],
|
||||
'customer_name' => $customer['mb_name'] ?? '고객',
|
||||
'estimate_id' => $bid_info['estimate_id'],
|
||||
'bid_amount' => number_format($bid_info['total_amount']),
|
||||
'selected_date' => date('Y-m-d H:i'),
|
||||
'estimate_url' => get_pretty_url('order', $wr_id)
|
||||
];
|
||||
|
||||
// 4. 알림 발송
|
||||
if (function_exists('notify_agent')) {
|
||||
notify_agent('bid_selected_email', $vars);
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => '대리점에게 견적 선택 알림을 발송했습니다.'];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("DealerNotification::sendBidSelectedNotification Error: " . $e->getMessage());
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시공 일정 알림 발송
|
||||
*
|
||||
* @param int $wr_id 게시물 ID
|
||||
* @return array ['success' => bool, 'message' => string]
|
||||
*/
|
||||
public function sendScheduleReminder($wr_id)
|
||||
{
|
||||
try {
|
||||
// 1. 시공 정보 조회
|
||||
$schedule_info = $this->estimateManager->getConstructionSchedule($wr_id);
|
||||
if (!$schedule_info || !$schedule_info['has_schedule']) {
|
||||
throw new Exception('시공 일정이 없습니다.');
|
||||
}
|
||||
|
||||
// 2. 대리점 정보 조회
|
||||
$dealer = get_member($schedule_info['dealer_id']);
|
||||
if (!$dealer) {
|
||||
throw new Exception('대리점 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 3. 알림 변수 설정
|
||||
$vars = [
|
||||
'dealer_name' => $dealer['mb_name'],
|
||||
'customer_name' => $schedule_info['customer_name'],
|
||||
'estimate_id' => $schedule_info['estimate_id'],
|
||||
'construction_date' => $schedule_info['construction_date'],
|
||||
'interim_payment_date' => $schedule_info['interim_payment_date']
|
||||
];
|
||||
|
||||
// 4. 알림 발송
|
||||
if (function_exists('notify_agent')) {
|
||||
notify_agent('schedule_reminder_email', $vars);
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => '대리점에게 시공 일정 알림을 발송했습니다.'];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("DealerNotification::sendScheduleReminder Error: " . $e->getMessage());
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 중도금 입금 기한 알림 발송
|
||||
*
|
||||
* @param int $wr_id 게시물 ID
|
||||
* @return array ['success' => bool, 'message' => string]
|
||||
*/
|
||||
public function sendInterimPaymentDueNotification($wr_id)
|
||||
{
|
||||
try {
|
||||
// 1. 중도금 입금 타이밍 확인
|
||||
$timing_info = $this->estimateManager->checkInterimPaymentTiming($wr_id);
|
||||
if (!$timing_info['is_due']) {
|
||||
return ['success' => true, 'message' => '중도금 입금 기한이 아직 도래하지 않았습니다.'];
|
||||
}
|
||||
|
||||
// 2. 시공 정보 조회
|
||||
$schedule_info = $this->estimateManager->getConstructionSchedule($wr_id);
|
||||
if (!$schedule_info) {
|
||||
throw new Exception('시공 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 3. 대리점 정보 조회
|
||||
$dealer = get_member($schedule_info['dealer_id']);
|
||||
if (!$dealer) {
|
||||
throw new Exception('대리점 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 4. 알림 변수 설정
|
||||
$vars = [
|
||||
'dealer_name' => $dealer['mb_name'],
|
||||
'customer_name' => $schedule_info['customer_name'],
|
||||
'estimate_id' => $schedule_info['estimate_id'],
|
||||
'construction_date' => $schedule_info['construction_date'],
|
||||
'interim_payment_date' => $schedule_info['interim_payment_date'],
|
||||
'days_remaining' => $timing_info['days_remaining']
|
||||
];
|
||||
|
||||
// 5. 알림 발송
|
||||
if (function_exists('notify_agent')) {
|
||||
notify_agent('interim_due_email', $vars);
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => '대리점에게 중도금 입금 기한 알림을 발송했습니다.'];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("DealerNotification::sendInterimPaymentDueNotification Error: " . $e->getMessage());
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
$sub_menu = "800200";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$g5['title'] = '견적 관리';
|
||||
include_once('./admin.head.php');
|
||||
|
||||
// 검색 조건
|
||||
$where = " WHERE 1=1 ";
|
||||
$search_params = '';
|
||||
|
||||
if (isset($_GET['status']) && $_GET['status']) {
|
||||
$status = sql_real_escape_string($_GET['status']);
|
||||
$where .= " AND (e.status = '{$status}' OR w.wr_1 = '{$status}')";
|
||||
$search_params .= "&status={$_GET['status']}";
|
||||
}
|
||||
|
||||
if (isset($_GET['stx']) && $_GET['stx']) {
|
||||
$stx = sql_real_escape_string($_GET['stx']);
|
||||
$where .= " AND (w.wr_subject LIKE '%{$stx}%' OR w.wr_name LIKE '%{$stx}%')";
|
||||
$search_params .= "&stx={$_GET['stx']}";
|
||||
}
|
||||
|
||||
$write_table = $g5['write_prefix'] . 'order';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.status-popup-overlay {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.5); z-index: 9999; display: none;
|
||||
}
|
||||
.status-popup {
|
||||
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
background: #fff; border: 1px solid #ddd; border-radius: 4px;
|
||||
min-width: 400px; z-index: 10000; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
.status-popup-header {
|
||||
padding: 15px 20px; border-bottom: 1px solid #eee; background: #f8f9fa;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.status-popup-header h3 { margin: 0; font-size: 16px; color: #333; font-weight: 600; }
|
||||
.status-popup-body { padding: 20px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: #333; }
|
||||
.form-control {
|
||||
width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 3px;
|
||||
font-size: 14px; box-sizing: border-box;
|
||||
}
|
||||
.status-popup-footer {
|
||||
padding: 15px 20px; border-top: 1px solid #eee; text-align: right;
|
||||
background: #f8f9fa; border-radius: 0 0 4px 4px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px; border: none; border-radius: 3px; cursor: pointer;
|
||||
font-size: 14px; margin-left: 5px; transition: all 0.2s;
|
||||
}
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-secondary { background: #6c757d; color: white; }
|
||||
.close-btn {
|
||||
float: right; background: none; border: none; font-size: 18px;
|
||||
cursor: pointer; color: #999; padding: 0; width: 20px; height: 20px;
|
||||
}
|
||||
|
||||
.status-견적신청중 { color: #6c757d; }
|
||||
.status-작성완료 { color: #28a745; }
|
||||
.status-견적제안 { color: #17a2b8; }
|
||||
.status-견적채택 { color: #ffc107; }
|
||||
.status-입금예정 { color: #fd7e14; }
|
||||
.status-입금확인 { color: #20c997; }
|
||||
.status-다운로드 { color: #6610f2; }
|
||||
.status-견적취소 { color: #dc3545; }
|
||||
|
||||
.status-btn {
|
||||
cursor: pointer; padding: 4px 8px; border-radius: 3px;
|
||||
background: #f8f9fa; border: 1px solid #dee2e6; font-size: 12px;
|
||||
}
|
||||
.status-btn:hover { background: #e9ecef; }
|
||||
.brand-info {
|
||||
display: inline-block; background: #e9ecef; padding: 2px 6px;
|
||||
border-radius: 3px; font-size: 11px; color: #495057; margin-right: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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 ?? 0) ?>건 </span></span>
|
||||
</div>
|
||||
|
||||
<form name="fsearch" method="get">
|
||||
<input type="hidden" name="sca" value="<?php echo $sca ?>">
|
||||
<div class="local_sch01 local_sch">
|
||||
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
|
||||
<select name="status">
|
||||
<option value="">전체상태</option>
|
||||
<option value="견적신청중" <?php echo ($_GET['status'] ?? '') === '견적신청중' ? 'selected' : ''; ?>>견적신청중</option>
|
||||
<option value="작성완료" <?php echo ($_GET['status'] ?? '') === '작성완료' ? 'selected' : ''; ?>>작성완료</option>
|
||||
<option value="견적제안" <?php echo ($_GET['status'] ?? '') === '견적제안' ? 'selected' : ''; ?>>견적제안</option>
|
||||
<option value="견적채택" <?php echo ($_GET['status'] ?? '') === '견적채택' ? 'selected' : ''; ?>>견적채택</option>
|
||||
<option value="입금예정" <?php echo ($_GET['status'] ?? '') === '입금예정' ? 'selected' : ''; ?>>입금예정</option>
|
||||
<option value="입금확인" <?php echo ($_GET['status'] ?? '') === '입금확인' ? 'selected' : ''; ?>>입금확인</option>
|
||||
</select>
|
||||
<input type="text" name="stx" value="<?php echo $_GET['stx'] ?? '' ?>" placeholder="제목, 작성자 검색">
|
||||
<input type="submit" class="btn_submit" value="검색">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form name="flist" id="flist" method="post" onsubmit="return flist_submit(this);">
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<label for="chkall" class="sound_only">전체</label>
|
||||
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
|
||||
</th>
|
||||
<th scope="col">번호</th>
|
||||
<th scope="col">제목</th>
|
||||
<th scope="col">작성자</th>
|
||||
<th scope="col">상태</th>
|
||||
<th scope="col">브랜드</th>
|
||||
<th scope="col">등록일</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// 목록 조회
|
||||
$sql = "SELECT w.*, e.status as estimate_status,
|
||||
(SELECT GROUP_CONCAT(DISTINCT brand)
|
||||
FROM estimate_item ei
|
||||
JOIN estimate est ON ei.estimate_id = est.id
|
||||
WHERE est.wr_id = w.wr_id AND brand IS NOT NULL AND brand != '') as brands
|
||||
FROM {$write_table} w
|
||||
LEFT JOIN estimate e ON w.wr_id = e.wr_id
|
||||
{$where}
|
||||
ORDER BY w.wr_num, w.wr_reply
|
||||
LIMIT 0, 30";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$listall = '<a href="'.$_SERVER['SCRIPT_NAME'].'" class="ov_listall">전체목록</a>';
|
||||
|
||||
for ($i=0; $row=sql_fetch_array($result); $i++) {
|
||||
$status = $row['wr_1'] ?: $row['estimate_status'] ?: '견적신청중';
|
||||
$brands = $row['brands'] ? explode(',', $row['brands']) : [];
|
||||
|
||||
// 사용자별 표시명
|
||||
$display_status = $status;
|
||||
if ($member['mb_level'] >= 8) {
|
||||
// 관리자용 표시명
|
||||
$display_status = $status;
|
||||
}
|
||||
?>
|
||||
<tr class="<?php echo $row['wr_parent'] ? 'reply' : 'original' ?>">
|
||||
<td>
|
||||
<input type="checkbox" name="chk_wr_id[]" value="<?php echo $row['wr_id'] ?>" id="chk_wr_id_<?php echo $i ?>">
|
||||
</td>
|
||||
<td><?php echo $row['wr_id'] ?></td>
|
||||
<td class="td_subject">
|
||||
<?php echo $row['wr_parent'] ? '└ ' : '' ?>
|
||||
<a href="<?php echo G5_BBS_URL ?>/board.php?bo_table=order&wr_id=<?php echo $row['wr_id'] ?>">
|
||||
<?php echo get_text($row['wr_subject']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?php echo $row['wr_name'] ?></td>
|
||||
<td class="text-center">
|
||||
<span class="status-btn status-<?php echo $status ?>"
|
||||
onclick="openStatusPopup(<?php echo $row['wr_id'] ?>, '<?php echo $status ?>', <?php echo $member['mb_level'] ?>, <?php echo !empty($row['wr_parent']) ? 'true' : 'false' ?>)">
|
||||
<?php echo $display_status ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
if ($brands) {
|
||||
foreach (array_slice($brands, 0, 2) as $brand) {
|
||||
echo '<span class="brand-info">' . htmlspecialchars(trim($brand)) . '</span>';
|
||||
}
|
||||
if (count($brands) > 2) {
|
||||
echo '<span class="brand-info">+' . (count($brands) - 2) . '</span>';
|
||||
}
|
||||
} else {
|
||||
echo '<span style="color:#999;">-</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo substr($row['wr_datetime'], 0, 10) ?></td>
|
||||
<td>
|
||||
<a href="<?php echo G5_BBS_URL ?>/board.php?bo_table=order&wr_id=<?php echo $row['wr_id'] ?>" class="btn btn_03">보기</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
if ($i == 0) {
|
||||
echo '<tr><td colspan="8" class="empty_table">자료가 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 상태 변경 팝업 -->
|
||||
<div id="statusPopupOverlay" class="status-popup-overlay">
|
||||
<div class="status-popup">
|
||||
<div class="status-popup-header">
|
||||
<h3>상태 변경</h3>
|
||||
<button type="button" class="close-btn" onclick="closeStatusPopup()">×</button>
|
||||
</div>
|
||||
<div class="status-popup-body">
|
||||
<form id="statusChangeForm">
|
||||
<input type="hidden" id="popup_wr_id" name="wr_id">
|
||||
<input type="hidden" name="action" value="change_status">
|
||||
|
||||
<div class="form-group">
|
||||
<label>현재 상태:</label>
|
||||
<div id="current_status_display" style="color:#666; font-weight:500;"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_status">변경할 상태:</label>
|
||||
<select id="new_status" name="new_status" class="form-control" required>
|
||||
<!-- JavaScript로 동적 생성 -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="memo">변경 사유 (선택):</label>
|
||||
<textarea id="memo" name="memo" class="form-control" placeholder="상태 변경 사유를 입력하세요"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="status-popup-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeStatusPopup()">취소</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitStatusChange()">변경</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 상태 전환 규칙
|
||||
const statusTransitions = {
|
||||
admin: {
|
||||
original: {
|
||||
'견적신청중': ['작성완료', '견적취소'],
|
||||
'작성완료': ['견적신청중', '입금예정'],
|
||||
'견적채택': ['입금예정'],
|
||||
'입금예정': ['입금확인'],
|
||||
'입금확인': ['다운로드'],
|
||||
'다운로드': ['견적신청중']
|
||||
},
|
||||
reply: {
|
||||
'견적제안': ['견적채택', '견적취소'],
|
||||
'견적채택': ['견적제안'],
|
||||
'견적취소': ['견적제안']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let currentWrId, currentStatus, currentUserLevel, isReply;
|
||||
|
||||
function openStatusPopup(wrId, status, userLevel, isReplyPost) {
|
||||
currentWrId = wrId;
|
||||
currentStatus = status;
|
||||
currentUserLevel = userLevel;
|
||||
isReply = isReplyPost;
|
||||
|
||||
document.getElementById('popup_wr_id').value = wrId;
|
||||
document.getElementById('current_status_display').textContent = status;
|
||||
|
||||
const newStatusSelect = document.getElementById('new_status');
|
||||
newStatusSelect.innerHTML = '';
|
||||
|
||||
const postType = isReplyPost ? 'reply' : 'original';
|
||||
const availableStatuses = statusTransitions.admin[postType][status] || [];
|
||||
|
||||
if (availableStatuses.length === 0) {
|
||||
newStatusSelect.innerHTML = '<option value="">변경 가능한 상태가 없습니다</option>';
|
||||
} else {
|
||||
availableStatuses.forEach(statusValue => {
|
||||
const option = document.createElement('option');
|
||||
option.value = statusValue;
|
||||
option.textContent = statusValue;
|
||||
newStatusSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('statusPopupOverlay').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeStatusPopup() {
|
||||
document.getElementById('statusPopupOverlay').style.display = 'none';
|
||||
}
|
||||
|
||||
function submitStatusChange() {
|
||||
const form = document.getElementById('statusChangeForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const newStatus = document.getElementById('new_status').value;
|
||||
if (!newStatus) {
|
||||
alert('변경할 상태를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('상태를 "' + newStatus + '"로 변경하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('./list.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
closeStatusPopup();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('오류: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('서버 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('statusPopupOverlay').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeStatusPopup();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once('./admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
include_once('../../../_common.php');
|
||||
|
||||
// 알림 시스템 로드
|
||||
if (file_exists(G5_LIB_PATH . '/notification_helper.php')) {
|
||||
include_once(G5_LIB_PATH . '/notification_helper.php');
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'message' => 'POST 요청만 허용됩니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$wr_id = (int) ($_POST['wr_id'] ?? 0);
|
||||
$new_status = trim($_POST['new_status'] ?? '');
|
||||
$memo = trim($_POST['memo'] ?? '');
|
||||
|
||||
if (!$wr_id || !$new_status) {
|
||||
echo json_encode(['success' => false, 'message' => '필수 파라미터가 누락되었습니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$is_member) {
|
||||
echo json_encode(['success' => false, 'message' => '로그인이 필요합니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_admin = ($member['mb_level'] ?? 0) >= 8;
|
||||
$is_agent = in_array(($member['mb_level'] ?? 0), [5, 6, 7]);
|
||||
|
||||
try {
|
||||
sql_query("START TRANSACTION");
|
||||
|
||||
$write_table = $g5['write_prefix'] . 'order';
|
||||
$write = sql_fetch("SELECT * FROM {$write_table} WHERE wr_id = '{$wr_id}'");
|
||||
|
||||
if (!$write) {
|
||||
throw new Exception('게시물을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$is_owner = ($member['mb_id'] === $write['mb_id']);
|
||||
|
||||
if (!$is_admin && !$is_owner && !$is_agent) {
|
||||
throw new Exception('권한이 없습니다.');
|
||||
}
|
||||
|
||||
$current_estimate = sql_fetch("SELECT * FROM estimate WHERE wr_id = '{$wr_id}'");
|
||||
$old_status = $current_estimate ? $current_estimate['status'] : '견적신청중';
|
||||
|
||||
// 권한별 상태 변경 검증
|
||||
$allowed = false;
|
||||
if ($is_admin) {
|
||||
$allowed = true; // 관리자는 모든 상태 변경 가능
|
||||
} elseif ($is_owner && empty($write['wr_parent'])) {
|
||||
// 고객 (원본글 작성자)
|
||||
if ($old_status === '견적신청중' && $new_status === '작성완료') {
|
||||
$allowed = true;
|
||||
} elseif ($old_status === '작성완료' && $new_status === '견적채택') {
|
||||
$allowed = true;
|
||||
}
|
||||
} elseif ($is_agent && !empty($write['wr_parent']) && $write['mb_id'] === $member['mb_id']) {
|
||||
// 대리점 (답글 작성자)
|
||||
if ($old_status === '견적제안' && in_array($new_status, ['견적채택', '견적취소'])) {
|
||||
$allowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allowed) {
|
||||
throw new Exception('해당 상태로 변경할 권한이 없습니다.');
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
if ($current_estimate) {
|
||||
sql_query("UPDATE estimate SET
|
||||
status = '{$new_status}',
|
||||
updated_at = NOW(),
|
||||
updated_by = '{$member['mb_id']}'
|
||||
WHERE wr_id = '{$wr_id}'");
|
||||
} else {
|
||||
sql_query("INSERT INTO estimate (wr_id, status, created_at, created_by, updated_at, updated_by)
|
||||
VALUES ('{$wr_id}', '{$new_status}', NOW(), '{$member['mb_id']}', NOW(), '{$member['mb_id']}')");
|
||||
}
|
||||
|
||||
// 게시판 wr_1 필드도 업데이트
|
||||
sql_query("UPDATE {$write_table} SET wr_1 = '{$new_status}' WHERE wr_id = '{$wr_id}'");
|
||||
|
||||
// 이력 기록
|
||||
$history_data = json_encode([
|
||||
'old_status' => $old_status,
|
||||
'new_status' => $new_status,
|
||||
'changed_by' => $member['mb_id'],
|
||||
'changed_at' => date('Y-m-d H:i:s'),
|
||||
'memo' => $memo,
|
||||
'ip' => $_SERVER['REMOTE_ADDR']
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$estimate = sql_fetch("SELECT id FROM estimate WHERE wr_id = '{$wr_id}'");
|
||||
$estimate_id = $estimate ? $estimate['id'] : 0;
|
||||
|
||||
sql_query("INSERT INTO estimate_history (
|
||||
estimate_id, action, change_details, changed_by, changed_at
|
||||
) VALUES (
|
||||
'{$estimate_id}', 'status_change', '{$history_data}', '{$member['mb_id']}', NOW()
|
||||
)");
|
||||
|
||||
// 알림 발송
|
||||
processStatusChangeNotification($write, $old_status, $new_status, $member);
|
||||
|
||||
// 견적채택 특별 처리
|
||||
if ($new_status === '견적채택' && !empty($write['wr_parent'])) {
|
||||
handleQuoteSelection($write, $member);
|
||||
}
|
||||
|
||||
sql_query("COMMIT");
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '상태가 성공적으로 변경되었습니다.',
|
||||
'data' => [
|
||||
'old_status' => $old_status,
|
||||
'new_status' => $new_status
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
sql_query("ROLLBACK");
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
function processStatusChangeNotification($write, $old_status, $new_status, $member) {
|
||||
switch ($new_status) {
|
||||
case '작성완료':
|
||||
notifyAgentsNewRequest($write);
|
||||
break;
|
||||
case '견적제안':
|
||||
notifyCustomerQuoteReceived($write);
|
||||
break;
|
||||
case '입금확인':
|
||||
notifyPaymentConfirmed($write, $member);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function notifyAgentsNewRequest($write) {
|
||||
$agents_sql = "SELECT mb_id, mb_name, mb_email, mb_hp FROM {$GLOBALS['g5']['member_table']}
|
||||
WHERE mb_level IN (5,6,7) AND mb_leave_date = '' AND mb_intercept_date = ''";
|
||||
$agents = sql_query($agents_sql);
|
||||
|
||||
while ($agent = sql_fetch_array($agents)) {
|
||||
$subject = "[견적요청] 새로운 견적 요청이 등록되었습니다";
|
||||
$content = "안녕하세요 {$agent['mb_name']}님,\n\n새로운 견적 요청이 등록되었습니다.\n\n고객명: {$write['wr_name']}\n요청제목: {$write['wr_subject']}\n등록시간: " . date('Y-m-d H:i') . "\n\n확인 URL: " . G5_HTTP_BBS_URL . "/board.php?bo_table=order&wr_id={$write['wr_id']}\n\n감사합니다.";
|
||||
|
||||
@mailer($agent['mb_name'], $agent['mb_email'], $subject, $content, 1);
|
||||
|
||||
if ($agent['mb_hp']) {
|
||||
@send_sms($agent['mb_hp'], "[견적요청] {$write['wr_name']}님의 새 견적요청이 등록되었습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifyCustomerQuoteReceived($write) {
|
||||
$parent_write = sql_fetch("SELECT * FROM {$GLOBALS['g5']['write_prefix']}order WHERE wr_id = '{$write['wr_parent']}'");
|
||||
|
||||
if ($parent_write) {
|
||||
$customer = get_member($parent_write['mb_id']);
|
||||
$agent_name = get_member_name($write['mb_id']);
|
||||
|
||||
@mailer($parent_write['wr_name'], $customer['mb_email'], "[견적도착] {$agent_name}님이 견적을 제안했습니다",
|
||||
"안녕하세요 {$parent_write['wr_name']}님,\n\n{$agent_name}님이 견적을 제안했습니다.\n\n확인해주세요.", 1);
|
||||
|
||||
if ($customer['mb_hp']) {
|
||||
@send_sms($customer['mb_hp'], "[견적도착] {$agent_name}님이 견적을 제안했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifyPaymentConfirmed($write, $admin_member) {
|
||||
$customer = get_member($write['mb_id']);
|
||||
@mailer($write['wr_name'], $customer['mb_email'], "[입금확인] 입금이 확인되었습니다",
|
||||
"안녕하세요 {$write['wr_name']}님,\n\n입금이 확인되었습니다.\n\n감사합니다.", 1);
|
||||
|
||||
if ($customer['mb_hp']) {
|
||||
@send_sms($customer['mb_hp'], "[입금확인] 입금이 확인되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuoteSelection($write, $member) {
|
||||
$origin_wr_id = $write['wr_parent'];
|
||||
$write_table = $GLOBALS['g5']['write_prefix'] . 'order';
|
||||
|
||||
sql_query("UPDATE estimate SET status = '입금예정' WHERE wr_id = '{$origin_wr_id}'");
|
||||
sql_query("UPDATE {$write_table} SET wr_1 = '견적취소' WHERE wr_parent = '{$origin_wr_id}' AND wr_id != '{$write['wr_id']}'");
|
||||
|
||||
$agent = get_member($write['mb_id']);
|
||||
@mailer($agent['mb_name'], $agent['mb_email'], "[견적채택] 축하합니다!", "견적이 채택되었습니다.", 1);
|
||||
|
||||
if ($agent['mb_hp']) {
|
||||
@send_sms($agent['mb_hp'], "[견적채택] 축하합니다! 견적이 채택되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
function get_member_name($mb_id) {
|
||||
if (!$mb_id) return '';
|
||||
$member = sql_fetch("SELECT mb_name FROM {$GLOBALS['g5']['member_table']} WHERE mb_id = '{$mb_id}'");
|
||||
return $member ? $member['mb_name'] : $mb_id;
|
||||
}
|
||||
|
||||
function send_sms($phone, $message) {
|
||||
return true;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
$sub_menu = "800760";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$g5['title'] = 'EstimateManager 테스트';
|
||||
|
||||
// EstimateManager 클래스 로드
|
||||
require_once G5_PATH . '/adm/order_manage/classes/EstimateManager.class.php';
|
||||
|
||||
$estimate_manager = new EstimateManager();
|
||||
$test_results = [];
|
||||
|
||||
// 테스트 실행
|
||||
if ($_POST['action'] === 'test') {
|
||||
$test_type = trim($_POST['test_type']);
|
||||
$wr_id = (int) $_POST['wr_id'];
|
||||
|
||||
switch ($test_type) {
|
||||
case 'complete_estimate':
|
||||
$result = $estimate_manager->completeEstimate($wr_id, 'customer');
|
||||
$test_results[] = $result;
|
||||
break;
|
||||
|
||||
case 'calculate_payment':
|
||||
$estimate_id = (int) $_POST['estimate_id'];
|
||||
$payment_type = trim($_POST['payment_type']);
|
||||
$result = $estimate_manager->calculatePaymentAmount($estimate_id, $payment_type);
|
||||
$test_results[] = [
|
||||
'success' => $result !== false,
|
||||
'data' => $result,
|
||||
'message' => $result !== false ? '결제 금액 계산 성공' : '결제 금액 계산 실패'
|
||||
];
|
||||
break;
|
||||
|
||||
case 'get_details':
|
||||
$result = $estimate_manager->getEstimateDetails($wr_id, 'admin', $member['mb_id']);
|
||||
$test_results[] = [
|
||||
'success' => $result !== false,
|
||||
'data' => $result,
|
||||
'message' => $result !== false ? '견적 정보 조회 성공' : '견적 정보 조회 실패'
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 견적 목록 조회 (테스트용)
|
||||
$recent_estimates = [];
|
||||
$sql = "
|
||||
SELECT w.wr_id, w.wr_subject, w.mb_id, e.id as estimate_id, e.status, e.updated_at, e.selected_bid_id
|
||||
FROM {$g5['write_prefix']}order w
|
||||
LEFT JOIN estimate e ON w.wr_id = e.wr_id
|
||||
WHERE w.wr_parent = 0
|
||||
ORDER BY w.wr_datetime DESC
|
||||
LIMIT 10
|
||||
";
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$recent_estimates[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
EstimateManager 클래스의 견적 관리 기능을 테스트합니다.<br>
|
||||
StatusManager와 연동된 기능들을 확인할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($test_results)): ?>
|
||||
<div class="local_desc02 local_desc">
|
||||
<h3>테스트 결과</h3>
|
||||
<?php foreach ($test_results as $result): ?>
|
||||
<div
|
||||
style="padding: 15px; margin: 10px 0; border: 1px solid <?php echo $result['success'] ? '#28a745' : '#dc3545'; ?>; background: <?php echo $result['success'] ? '#d4edda' : '#f8d7da'; ?>;">
|
||||
<strong><?php echo $result['success'] ? '성공' : '실패'; ?>:</strong>
|
||||
<?php echo htmlspecialchars($result['message']); ?>
|
||||
<?php if (isset($result['data']) && $result['data']): ?>
|
||||
<br><br><strong>데이터:</strong>
|
||||
<pre
|
||||
style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 12px; max-height: 300px; overflow-y: auto;"><?php echo htmlspecialchars(json_encode($result['data'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); ?></pre>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form name="ftest" method="post" action="./estimate_test.php">
|
||||
<input type="hidden" name="action" value="test">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>EstimateManager 테스트</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="test_type">테스트 유형</label></th>
|
||||
<td>
|
||||
<select name="test_type" id="test_type" class="frm_input" onchange="toggleTestFields()">
|
||||
<option value="">선택하세요</option>
|
||||
<option value="complete_estimate">견적 작성 완료</option>
|
||||
<option value="calculate_payment">결제 금액 계산</option>
|
||||
<option value="get_details">견적 상세 정보 조회</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="wr_id">게시물 ID</label></th>
|
||||
<td>
|
||||
<select name="wr_id" id="wr_id" class="frm_input">
|
||||
<option value="">선택하세요</option>
|
||||
<?php foreach ($recent_estimates as $estimate): ?>
|
||||
<option value="<?php echo $estimate['wr_id']; ?>"
|
||||
data-estimate-id="<?php echo $estimate['estimate_id']; ?>">
|
||||
<?php echo $estimate['wr_id']; ?> -
|
||||
<?php echo htmlspecialchars($estimate['wr_subject']); ?>
|
||||
(상태: <?php echo $estimate['status'] ?: '견적신청중'; ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="estimate_id_row" style="display: none;">
|
||||
<th scope="row"><label for="estimate_id">견적서 ID</label></th>
|
||||
<td>
|
||||
<input type="number" name="estimate_id" id="estimate_id" class="frm_input">
|
||||
<div class="frm_info">결제 금액 계산 시 필요</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="payment_type_row" style="display: none;">
|
||||
<th scope="row"><label for="payment_type">결제 타입</label></th>
|
||||
<td>
|
||||
<select name="payment_type" id="payment_type" class="frm_input">
|
||||
<option value="deposit">계약금</option>
|
||||
<option value="interim">중도금</option>
|
||||
<option value="final">잔금</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="테스트 실행" class="btn_submit">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<h3>최근 견적 목록</h3>
|
||||
<table class="tbl_head01 tbl_wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>게시물 ID</th>
|
||||
<th>견적서 ID</th>
|
||||
<th>제목</th>
|
||||
<th>작성자</th>
|
||||
<th>현재 상태</th>
|
||||
<th>선택된 입찰</th>
|
||||
<th>수정일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent_estimates as $estimate): ?>
|
||||
<tr>
|
||||
<td><?php echo $estimate['wr_id']; ?></td>
|
||||
<td><?php echo $estimate['estimate_id'] ?: '-'; ?></td>
|
||||
<td><?php echo htmlspecialchars($estimate['wr_subject']); ?></td>
|
||||
<td><?php echo htmlspecialchars($estimate['mb_id']); ?></td>
|
||||
<td><?php echo $estimate['status'] ?: '견적신청중'; ?></td>
|
||||
<td><?php echo $estimate['selected_bid_id'] ?: '-'; ?></td>
|
||||
<td><?php echo $estimate['updated_at'] ?: '-'; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<h3>EstimateManager 주요 기능</h3>
|
||||
<ul>
|
||||
<li><strong>StatusManager 연동:</strong> 체계적인 상태 관리와 연동</li>
|
||||
<li><strong>견적 생명주기 관리:</strong> 생성 → 완료 → 선택 → 결제 전체 프로세스</li>
|
||||
<li><strong>권한별 데이터 접근:</strong> 사용자 역할에 따른 정보 필터링</li>
|
||||
<li><strong>대리점 입찰 시스템:</strong> 시스템 비용 자동 계산 포함</li>
|
||||
<li><strong>결제 관리:</strong> 단계별 결제 금액 계산 및 상태 관리</li>
|
||||
<li><strong>트랜잭션 처리:</strong> 데이터 무결성 보장</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleTestFields() {
|
||||
const testType = document.getElementById('test_type').value;
|
||||
const estimateIdRow = document.getElementById('estimate_id_row');
|
||||
const paymentTypeRow = document.getElementById('payment_type_row');
|
||||
|
||||
// 모든 필드 숨기기
|
||||
estimateIdRow.style.display = 'none';
|
||||
paymentTypeRow.style.display = 'none';
|
||||
|
||||
// 테스트 유형에 따라 필요한 필드 표시
|
||||
if (testType === 'calculate_payment') {
|
||||
estimateIdRow.style.display = '';
|
||||
paymentTypeRow.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 게시물 선택 시 견적서 ID 자동 설정
|
||||
document.getElementById('wr_id').addEventListener('change', function () {
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
const estimateId = selectedOption.getAttribute('data-estimate-id');
|
||||
if (estimateId) {
|
||||
document.getElementById('estimate_id').value = estimateId;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
require_once './_common.php';
|
||||
|
||||
// 관리자 권한 체크
|
||||
if (!$is_admin) {
|
||||
die('관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
echo "<h2>견적서 시스템 업데이트 실행</h2>";
|
||||
|
||||
// SQL 파일 읽기
|
||||
$sql_file = './update_for_new_features.sql';
|
||||
if (!file_exists($sql_file)) {
|
||||
die('SQL 파일을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$sql_content = file_get_contents($sql_file);
|
||||
|
||||
// SQL 문을 세미콜론으로 분리
|
||||
$sql_statements = explode(';', $sql_content);
|
||||
|
||||
$success_count = 0;
|
||||
$error_count = 0;
|
||||
$errors = array();
|
||||
|
||||
echo "<div style='background: #f5f5f5; padding: 10px; margin: 10px 0;'>";
|
||||
echo "<h3>업데이트 진행 상황:</h3>";
|
||||
|
||||
foreach ($sql_statements as $sql) {
|
||||
$sql = trim($sql);
|
||||
|
||||
// 빈 문장이나 주석 건너뛰기
|
||||
if (empty($sql) || strpos($sql, '--') === 0 || strpos($sql, '/*') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = sql_query($sql, false);
|
||||
if ($result) {
|
||||
$success_count++;
|
||||
echo "<p style='color: green;'>✓ SQL 실행 성공: " . substr($sql, 0, 50) . "...</p>";
|
||||
} else {
|
||||
$error_count++;
|
||||
$error_msg = sql_error();
|
||||
$errors[] = "SQL: " . substr($sql, 0, 100) . "... | 오류: " . $error_msg;
|
||||
echo "<p style='color: orange;'>⚠ SQL 실행 건너뜀 (이미 존재하거나 무시): " . substr($sql, 0, 50) . "...</p>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error_count++;
|
||||
$errors[] = "SQL: " . substr($sql, 0, 100) . "... | 오류: " . $e->getMessage();
|
||||
echo "<p style='color: red;'>✗ SQL 실행 실패: " . substr($sql, 0, 50) . "... | 오류: " . $e->getMessage() . "</p>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</div>";
|
||||
|
||||
echo "<h3>업데이트 결과:</h3>";
|
||||
echo "<p>성공: {$success_count}개</p>";
|
||||
echo "<p>오류/건너뜀: {$error_count}개</p>";
|
||||
|
||||
if (!empty($errors)) {
|
||||
echo "<h4>오류 상세:</h4>";
|
||||
echo "<ul>";
|
||||
foreach ($errors as $error) {
|
||||
echo "<li>" . htmlspecialchars($error) . "</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
|
||||
// 테이블 생성 확인
|
||||
echo "<h3>테이블 생성 확인:</h3>";
|
||||
$tables_to_check = array('order_config', 'order_mail_templates', 'order_sms_templates');
|
||||
|
||||
foreach ($tables_to_check as $table) {
|
||||
$full_table_name = G5_TABLE_PREFIX . $table;
|
||||
$result = sql_query("SHOW TABLES LIKE '{$full_table_name}'", false);
|
||||
if ($result && sql_num_rows($result) > 0) {
|
||||
echo "<p style='color: green;'>✓ {$table} 테이블 존재</p>";
|
||||
|
||||
// 데이터 개수 확인
|
||||
$count_result = sql_query("SELECT COUNT(*) as cnt FROM {$full_table_name}", false);
|
||||
if ($count_result) {
|
||||
$count_row = sql_fetch_array($count_result);
|
||||
echo "<p style='margin-left: 20px;'>- 데이터 개수: {$count_row['cnt']}개</p>";
|
||||
}
|
||||
} else {
|
||||
echo "<p style='color: red;'>✗ {$table} 테이블 없음</p>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
echo "<p><a href='./config_index.php'>설정 관리로 이동</a></p>";
|
||||
echo "<p><a href='./expert_visits.php'>전문가 방문 관리로 이동</a></p>";
|
||||
echo "<p><a href='./mail_templates.php'>메일 템플릿 관리로 이동</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
$sub_menu = "800800";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '전문가 방문 예약 관리';
|
||||
|
||||
// lib/notification_helper.php 포함 (설정값 가져오기 위해)
|
||||
// 경로 수정: G5_LIB_PATH 대신 현재 디렉토리 기준 lib 폴더 참조
|
||||
if (file_exists(__DIR__ . '/lib/notification_helper.php')) {
|
||||
include_once(__DIR__ . '/lib/notification_helper.php');
|
||||
}
|
||||
|
||||
function get_reservation_status_name($status_code) {
|
||||
// get_order_config 함수가 정의되어 있는지 확인 후 호출
|
||||
if (function_exists('get_order_config')) {
|
||||
return get_order_config('reservation_status_' . $status_code, $status_code);
|
||||
}
|
||||
return $status_code;
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
$sql_search = "";
|
||||
$sfl = isset($_GET['sfl']) ? clean_xss_tags($_GET['sfl']) : '';
|
||||
$stx = isset($_GET['stx']) ? clean_xss_tags($_GET['stx']) : '';
|
||||
$sst = isset($_GET['sst']) ? clean_xss_tags($_GET['sst']) : 'id';
|
||||
$sod = isset($_GET['sod']) ? clean_xss_tags($_GET['sod']) : 'desc';
|
||||
$status_filter = isset($_GET['status_filter']) ? clean_xss_tags($_GET['status_filter']) : '';
|
||||
|
||||
if ($stx) {
|
||||
$stx_escaped = sql_real_escape_string($stx);
|
||||
if ($sfl === 'customer_name') {
|
||||
$sql_search .= " AND customer_name LIKE '%{$stx_escaped}%' ";
|
||||
} elseif ($sfl === 'customer_phone') {
|
||||
$sql_search .= " AND customer_phone LIKE '%{$stx_escaped}%' ";
|
||||
}
|
||||
}
|
||||
|
||||
if ($status_filter) {
|
||||
$sql_search .= " AND status = '" . sql_real_escape_string($status_filter) . "' ";
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sql_order = " ORDER BY {$sst} {$sod} ";
|
||||
|
||||
// 페이징
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$page_rows = 15;
|
||||
$from_record = ($page - 1) * $page_rows;
|
||||
|
||||
// 전체 개수
|
||||
$sql_count = "SELECT COUNT(*) as cnt FROM expert_visit_reservations WHERE is_deleted = 0 {$sql_search}";
|
||||
$count_row = sql_fetch($sql_count);
|
||||
$total_count = $count_row['cnt'];
|
||||
$total_page = ceil($total_count / $page_rows);
|
||||
|
||||
// 데이터 조회
|
||||
$sql = "SELECT * FROM expert_visit_reservations WHERE is_deleted = 0 {$sql_search} {$sql_order} LIMIT {$from_record}, {$page_rows}";
|
||||
$result = sql_query($sql);
|
||||
$reservations = [];
|
||||
while($row = sql_fetch_array($result)) {
|
||||
$reservations[] = $row;
|
||||
}
|
||||
|
||||
// 전문가 목록 조회 (예: 레벨 8)
|
||||
$experts_result = sql_query("SELECT mb_id, mb_name FROM {$g5['member_table']} WHERE mb_level = 8 AND mb_leave_date = '' ORDER BY mb_name ASC");
|
||||
$experts = [];
|
||||
while($row = sql_fetch_array($experts_result)) {
|
||||
$experts[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
// jQuery 로드 확인 및 추가 (오류 방지)
|
||||
?>
|
||||
|
||||
<?php
|
||||
|
||||
$qstr = http_build_query([
|
||||
'sfl' => $sfl,
|
||||
'stx' => $stx,
|
||||
'sst' => $sst,
|
||||
'sod' => $sod,
|
||||
'status_filter' => $status_filter
|
||||
]);
|
||||
?>
|
||||
|
||||
<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>
|
||||
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<label for="status_filter" class="sound_only">상태 필터</label>
|
||||
<select name="status_filter" id="status_filter">
|
||||
<option value="">상태 전체</option>
|
||||
<option value="payment_pending" <?php echo get_selected($status_filter, 'payment_pending'); ?>><?php echo get_reservation_status_name('payment_pending'); ?></option>
|
||||
<option value="reserved" <?php echo get_selected($status_filter, 'reserved'); ?>><?php echo get_reservation_status_name('reserved'); ?></option>
|
||||
<option value="completed" <?php echo get_selected($status_filter, 'completed'); ?>><?php echo get_reservation_status_name('completed'); ?></option>
|
||||
<option value="cancelled" <?php echo get_selected($status_filter, 'cancelled'); ?>><?php echo get_reservation_status_name('cancelled'); ?></option>
|
||||
</select>
|
||||
|
||||
<label for="sfl" class="sound_only">검색대상</label>
|
||||
<select name="sfl" id="sfl">
|
||||
<option value="customer_name" <?php echo get_selected($sfl, 'customer_name'); ?>>고객명</option>
|
||||
<option value="customer_phone" <?php echo get_selected($sfl, 'customer_phone'); ?>>연락처</option>
|
||||
</select>
|
||||
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" class="frm_input">
|
||||
<input type="submit" class="btn_submit" value="검색">
|
||||
</form>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">예약번호</th>
|
||||
<th scope="col">고객명</th>
|
||||
<th scope="col">연락처</th>
|
||||
<th scope="col">방문일시</th>
|
||||
<th scope="col">방문종류</th>
|
||||
<th scope="col">상태</th>
|
||||
<th scope="col">담당전문가</th>
|
||||
<th scope="col">신청일</th>
|
||||
<th scope="col" style="width: 120px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($reservations)): ?>
|
||||
<tr><td colspan="9" class="empty_table">자료가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($reservations as $res): ?>
|
||||
<tr>
|
||||
<td><?php echo $res['id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($res['customer_name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($res['customer_phone']); ?></td>
|
||||
<td><?php echo $res['visit_date'] . ' ' . substr($res['visit_time'], 0, 5); ?></td>
|
||||
<td><?php echo htmlspecialchars($res['temp_2']); ?></td>
|
||||
<td><?php echo get_reservation_status_name($res['status']); ?></td>
|
||||
<td><?php echo $res['expert_id'] ? (get_member($res['expert_id'])['mb_name'] ?? $res['expert_id']) : '미배정'; ?></td>
|
||||
<td><?php echo substr($res['created_at'], 0, 10); ?></td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<button type="button" class="btn btn_03 manage-btn" data-id="<?php echo $res['id']; ?>">상세/관리</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'].'?'.$qstr.'&page='); ?>
|
||||
|
||||
<!-- 상세/관리 모달 -->
|
||||
<div id="reservationModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:1000;">
|
||||
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); background:white; padding:20px; border-radius:5px; width:600px;">
|
||||
<h3 id="modalTitle">전문가 방문 예약 상세/관리</h3>
|
||||
<form name="fmodal" id="fmodal">
|
||||
<input type="hidden" name="id" id="modal_id">
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">고객 정보</th>
|
||||
<td id="modal_customer_info"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">예약 정보</th>
|
||||
<td id="modal_reservation_info"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">방문 정보</th>
|
||||
<td id="modal_visit_info"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">고객 요청사항</th>
|
||||
<td><textarea id="modal_request_memo" readonly style="width:100%;height:60px;"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_status">상태 변경</label></th>
|
||||
<td>
|
||||
<select name="status" id="modal_status">
|
||||
<option value="payment_pending"><?php echo get_reservation_status_name('payment_pending'); ?></option>
|
||||
<option value="reserved"><?php echo get_reservation_status_name('reserved'); ?></option>
|
||||
<option value="completed"><?php echo get_reservation_status_name('completed'); ?></option>
|
||||
<option value="cancelled"><?php echo get_reservation_status_name('cancelled'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_expert_id">전문가 배정</label></th>
|
||||
<td>
|
||||
<select name="expert_id" id="modal_expert_id">
|
||||
<option value="">전문가 선택</option>
|
||||
<?php foreach ($experts as $c): ?>
|
||||
<option value="<?php echo $c['mb_id']; ?>"><?php echo htmlspecialchars($c['mb_name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_admin_memo">관리자 메모</label></th>
|
||||
<td><textarea name="admin_memo" id="modal_admin_memo" style="width:100%;height:80px;"></textarea></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn_confirm01 btn_confirm" style="margin-top:20px;">
|
||||
<button type="button" id="modalSaveBtn" class="btn_submit">저장</button>
|
||||
<button type="button" id="modalCloseBtn" class="btn_cancel">닫기</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = document.getElementById('reservationModal');
|
||||
const manageButtons = document.querySelectorAll('.manage-btn');
|
||||
const closeBtn = document.getElementById('modalCloseBtn');
|
||||
const saveBtn = document.getElementById('modalSaveBtn');
|
||||
|
||||
function openModal(id) {
|
||||
fetch('./expert_visit_reservations_ajax.php?action=get_details&id=' + id)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const res = data.data;
|
||||
document.getElementById('modal_id').value = res.id;
|
||||
document.getElementById('modalTitle').innerText = `전문가 방문 예약 상세/관리 (예약번호: ${res.id})`;
|
||||
document.getElementById('modal_customer_info').innerHTML = `${res.customer_name} (${res.customer_phone} / ${res.customer_email})`;
|
||||
document.getElementById('modal_reservation_info').innerHTML = `${res.visit_date} ${res.visit_time.substring(0,5)} / <strong>방문비: ${Number(res.payment_amount).toLocaleString()}원</strong>`;
|
||||
document.getElementById('modal_visit_info').innerHTML = `종류: ${res.temp_2} / 지역: ${res.temp_3} / 예상시간: ${res.temp_4}분`;
|
||||
document.getElementById('modal_request_memo').value = res.request_memo;
|
||||
document.getElementById('modal_status').value = res.status;
|
||||
document.getElementById('modal_expert_id').value = res.expert_id || '';
|
||||
document.getElementById('modal_admin_memo').value = res.admin_memo;
|
||||
modal.style.display = 'block';
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
manageButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
openModal(this.dataset.id);
|
||||
});
|
||||
});
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', function() {
|
||||
const form = document.getElementById('fmodal');
|
||||
const formData = new FormData(form);
|
||||
formData.append('action', 'update_reservation');
|
||||
|
||||
if (!confirm('변경 내용을 저장하시겠습니까?')) return;
|
||||
|
||||
fetch('./expert_visit_reservations_ajax.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams(formData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('오류: ' + data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
die(json_encode(['success' => false, 'message' => '관리자 권한이 필요합니다.']));
|
||||
}
|
||||
|
||||
// lib/notification_helper.php 포함
|
||||
if (file_exists(G5_LIB_PATH . '/notification_helper.php')) {
|
||||
include_once(G5_LIB_PATH . '/notification_helper.php');
|
||||
}
|
||||
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
$id = isset($_REQUEST['id']) ? (int)$_REQUEST['id'] : 0;
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'get_details':
|
||||
if (!$id) throw new Exception('예약 ID가 없습니다.');
|
||||
|
||||
$reservation = sql_fetch("SELECT * FROM expert_visit_reservations WHERE id = '{$id}'");
|
||||
if (!$reservation) throw new Exception('예약 정보를 찾을 수 없습니다.');
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $reservation]);
|
||||
break;
|
||||
|
||||
case 'update_reservation':
|
||||
if (!$id) throw new Exception('예약 ID가 없습니다.');
|
||||
|
||||
$status = isset($_POST['status']) ? clean_xss_tags($_POST['status']) : '';
|
||||
$expert_id = isset($_POST['expert_id']) ? clean_xss_tags($_POST['expert_id']) : '';
|
||||
$admin_memo = isset($_POST['admin_memo']) ? clean_xss_tags($_POST['admin_memo']) : '';
|
||||
|
||||
// 기존 정보 조회 (상태 변경 시 알림을 위함)
|
||||
$old_reservation = sql_fetch("SELECT status FROM expert_visit_reservations WHERE id = '{$id}'");
|
||||
|
||||
$sql = "UPDATE expert_visit_reservations SET " .
|
||||
" status = '" . sql_real_escape_string($status) . "', " .
|
||||
" expert_id = '" . sql_real_escape_string($expert_id) . "', " .
|
||||
" admin_memo = '" . sql_real_escape_string($admin_memo) . "', " .
|
||||
" updated_at = NOW(), " .
|
||||
" updated_by = '{$member['mb_id']}' " .
|
||||
" WHERE id = '{$id}' ";
|
||||
sql_query($sql);
|
||||
|
||||
// 상태 변경 시 알림 발송
|
||||
if (function_exists('notify_for_expert_visit')) {
|
||||
// 입금 확인 -> 예약 확정
|
||||
if ($old_reservation['status'] !== 'reserved' && $status === 'reserved') {
|
||||
notify_for_expert_visit($id, '예약확정');
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '방문 예약 정보가 성공적으로 업데이트되었습니다.']);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('유효하지 않은 요청입니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
$sub_menu = "800850"; // 메뉴 코드 (admin.menu800.order_manage.php와 일치)
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '전문가 방문 스케줄 관리';
|
||||
|
||||
// --- 액션 처리 (저장, 삭제) ---
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
$id = isset($_REQUEST['id']) ? (int)$_REQUEST['id'] : 0;
|
||||
|
||||
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
if ($action === 'save') {
|
||||
$expert_id = $_POST['expert_id'] ?? '';
|
||||
$rule_type = $_POST['rule_type'] ?? 'weekly';
|
||||
$day_of_week = ($rule_type === 'weekly') ? (int)($_POST['day_of_week'] ?? 0) : 'NULL';
|
||||
$specific_date = ($rule_type === 'specific') ? clean_xss_tags($_POST['specific_date']) : '';
|
||||
$start_time = clean_xss_tags($_POST['start_time']);
|
||||
$end_time = clean_xss_tags($_POST['end_time']);
|
||||
$time_slot = (int)$_POST['time_slot'];
|
||||
$max_persons = (int)$_POST['max_persons'];
|
||||
$is_available = (int)$_POST['is_available'];
|
||||
$temp_2 = clean_xss_tags($_POST['temp_2']); // 휴무사유
|
||||
$temp_3 = clean_xss_tags($_POST['temp_3']); // 특별일정명
|
||||
|
||||
$sql_expert_id = $expert_id ? "'" . sql_real_escape_string($expert_id) . "'" : "NULL";
|
||||
$sql_specific_date = $specific_date ? "'" . sql_real_escape_string($specific_date) . "'" : "NULL";
|
||||
|
||||
$sql_common = "
|
||||
expert_id = {$sql_expert_id},
|
||||
day_of_week = {$day_of_week},
|
||||
specific_date = {$sql_specific_date},
|
||||
start_time = '" . sql_real_escape_string($start_time) . "',
|
||||
end_time = '" . sql_real_escape_string($end_time) . "',
|
||||
time_slot = '{$time_slot}',
|
||||
max_persons = '{$max_persons}',
|
||||
is_available = '{$is_available}',
|
||||
temp_2 = '" . sql_real_escape_string($temp_2) . "',
|
||||
temp_3 = '" . sql_real_escape_string($temp_3) . "',
|
||||
updated_at = NOW(),
|
||||
updated_by = '{$member['mb_id']}'
|
||||
";
|
||||
|
||||
if ($id > 0) { // 수정
|
||||
$sql = "UPDATE expert_visit_schedules SET {$sql_common} WHERE id = '{$id}'";
|
||||
} else { // 생성
|
||||
$sql = "INSERT INTO expert_visit_schedules SET {$sql_common}, created_at = NOW(), created_by = '{$member['mb_id']}'";
|
||||
}
|
||||
sql_query($sql);
|
||||
goto_url('./expert_visit_schedule.php');
|
||||
|
||||
} elseif ($action === 'delete') {
|
||||
if ($id > 0) {
|
||||
sql_query("UPDATE expert_visit_schedules SET is_deleted = 1, updated_at = NOW(), updated_by = '{$member['mb_id']}' WHERE id = '{$id}'");
|
||||
}
|
||||
goto_url('./expert_visit_schedule.php');
|
||||
}
|
||||
}
|
||||
|
||||
$schedule_to_edit = null;
|
||||
if ($id > 0) {
|
||||
$schedule_to_edit = sql_fetch("SELECT * FROM expert_visit_schedules WHERE id = '{$id}'");
|
||||
}
|
||||
|
||||
// --- 데이터 조회 ---
|
||||
$experts_result = sql_query("SELECT mb_id, mb_name FROM {$g5['member_table']} WHERE mb_level = 8 AND mb_leave_date = '' ORDER BY mb_name ASC");
|
||||
$experts = [];
|
||||
while($row = sql_fetch_array($experts_result)) {
|
||||
$experts[] = $row;
|
||||
}
|
||||
|
||||
$schedules_result = sql_query("SELECT * FROM expert_visit_schedules WHERE is_deleted = 0 ORDER BY specific_date DESC, day_of_week ASC, start_time ASC");
|
||||
$schedules = [];
|
||||
while($row = sql_fetch_array($schedules_result)) {
|
||||
$schedules[] = $row;
|
||||
}
|
||||
|
||||
$week_days = [1 => '월요일', 2 => '화요일', 3 => '수요일', 4 => '목요일', 5 => '금요일', 6 => '토요일', 7 => '일요일'];
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
전문가가 방문 가능한 시간을 설정합니다. '요일별' 규칙은 주간 반복 스케줄이며, '특정일' 규칙은 해당 날짜에만 적용되는 우선순위가 높은 스케줄입니다.<br>
|
||||
'예약 가능'을 '아니오'로 설정하면 해당 시간을 휴무로 처리할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 스케줄 등록/수정 폼 -->
|
||||
<form name="fschedule" method="post">
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="id" value="<?php echo $schedule_to_edit['id'] ?? 0; ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $id ? '스케줄 수정' : '새 스케줄 등록'; ?></caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="expert_id">전문가</label></th>
|
||||
<td>
|
||||
<select name="expert_id" id="expert_id">
|
||||
<option value="">전체 전문가 공통</option>
|
||||
<?php foreach ($experts as $c): ?>
|
||||
<option value="<?php echo $c['mb_id']; ?>" <?php echo get_selected($schedule_to_edit['expert_id'] ?? '', $c['mb_id']); ?>><?php echo htmlspecialchars($c['mb_name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">규칙 종류</th>
|
||||
<td>
|
||||
<label><input type="radio" name="rule_type" value="weekly" <?php echo (($schedule_to_edit['specific_date'] ?? '') ? '' : 'checked'); ?>> 요일별</label>
|
||||
<label><input type="radio" name="rule_type" value="specific" <?php echo (($schedule_to_edit['specific_date'] ?? '') ? 'checked' : ''); ?>> 특정일</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="row_day_of_week">
|
||||
<th scope="row"><label for="day_of_week">요일</label></th>
|
||||
<td>
|
||||
<select name="day_of_week" id="day_of_week">
|
||||
<?php foreach ($week_days as $num => $day): ?>
|
||||
<option value="<?php echo $num; ?>" <?php echo get_selected($schedule_to_edit['day_of_week'] ?? '', $num); ?>><?php echo $day; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="row_specific_date" style="display:none;">
|
||||
<th scope="row"><label for="specific_date">특정 날짜</label></th>
|
||||
<td>
|
||||
<input type="text" name="specific_date" id="specific_date" value="<?php echo $schedule_to_edit['specific_date'] ?? ''; ?>" class="frm_input" style="width:120px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="start_time">방문 시간</label></th>
|
||||
<td>
|
||||
<input type="time" name="start_time" id="start_time" value="<?php echo $schedule_to_edit['start_time'] ?? '09:00'; ?>" required> ~
|
||||
<input type="time" name="end_time" id="end_time" value="<?php echo $schedule_to_edit['end_time'] ?? '18:00'; ?>" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="time_slot">예약 단위</label></th>
|
||||
<td>
|
||||
<input type="number" name="time_slot" id="time_slot" value="<?php echo $schedule_to_edit['time_slot'] ?? '60'; ?>" class="frm_input" style="width:80px;" required> 분
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="max_persons">최대 인원</label></th>
|
||||
<td>
|
||||
<input type="number" name="max_persons" id="max_persons" value="<?php echo $schedule_to_edit['max_persons'] ?? '1'; ?>" class="frm_input" style="width:80px;" required> 명
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="is_available">예약 가능</label></th>
|
||||
<td>
|
||||
<select name="is_available" id="is_available">
|
||||
<option value="1" <?php echo get_selected($schedule_to_edit['is_available'] ?? '1', '1'); ?>>예</option>
|
||||
<option value="0" <?php echo get_selected($schedule_to_edit['is_available'] ?? '1', '0'); ?>>아니오 (휴무)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="row_off_reason" style="display:none;">
|
||||
<th scope="row"><label for="temp_2">휴무 사유</label></th>
|
||||
<td>
|
||||
<input type="text" name="temp_2" id="temp_2" value="<?php echo htmlspecialchars($schedule_to_edit['temp_2'] ?? ''); ?>" class="frm_input" style="width:300px;" placeholder="예: 정기 휴무">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="temp_3">일정명</label></th>
|
||||
<td>
|
||||
<input type="text" name="temp_3" id="temp_3" value="<?php echo htmlspecialchars($schedule_to_edit['temp_3'] ?? ''); ?>" class="frm_input" style="width:300px;" placeholder="예: 오전 근무, 단축 운영">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="<?php echo $id ? '수정' : '등록'; ?>" class="btn_submit">
|
||||
<?php if ($id): ?>
|
||||
<a href="./expert_visit_schedule.php" class="btn_cancel">새로 등록</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 스케줄 목록 -->
|
||||
<div class="tbl_head01 tbl_wrap" style="margin-top: 30px;">
|
||||
<table>
|
||||
<caption>스케줄 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">전문가</th>
|
||||
<th scope="col">규칙</th>
|
||||
<th scope="col">시간</th>
|
||||
<th scope="col">예약단위</th>
|
||||
<th scope="col">최대인원</th>
|
||||
<th scope="col">상태</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($schedules)): ?>
|
||||
<tr><td colspan="8" class="empty_table">등록된 스케줄이 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($schedules as $sch): ?>
|
||||
<tr>
|
||||
<td><?php echo $sch['id']; ?></td>
|
||||
<td><?php echo $sch['expert_id'] ? (get_member($sch['expert_id'])['mb_name'] ?? $sch['expert_id']) : '전체 공통'; ?></td>
|
||||
<td>
|
||||
<?php
|
||||
if ($sch['specific_date']) {
|
||||
echo '<strong>[특정일]</strong> ' . $sch['specific_date'] . ($sch['temp_3'] ? ' ('.$sch['temp_3'].')' : '');
|
||||
} else {
|
||||
echo '<strong>[요일별]</strong> ' . ($week_days[$sch['day_of_week']] ?? '알수없음');
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo substr($sch['start_time'], 0, 5) . ' ~ ' . substr($sch['end_time'], 0, 5); ?></td>
|
||||
<td><?php echo $sch['time_slot']; ?>분</td>
|
||||
<td><?php echo $sch['max_persons']; ?>명</td>
|
||||
<td>
|
||||
<?php if ($sch['is_available']): ?>
|
||||
<span style="color:blue;">예약가능</span>
|
||||
<?php else: ?>
|
||||
<span style="color:red;">휴무</span>
|
||||
<?php if($sch['temp_2']) echo ' (' . htmlspecialchars($sch['temp_2']) . ')'; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<a href="?id=<?php echo $sch['id']; ?>" class="btn btn_03">수정</a>
|
||||
<form name="fdelete_<?php echo $sch['id']; ?>" method="post" style="display:inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?php echo $sch['id']; ?>">
|
||||
<button type="submit" class="btn btn_02" onclick="return confirm('정말로 이 스케줄을 삭제하시겠습니까?');">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 날짜 선택기 초기화
|
||||
flatpickr("#specific_date", { dateFormat: "Y-m-d" });
|
||||
|
||||
const ruleTypeRadios = document.querySelectorAll('input[name="rule_type"]');
|
||||
const dayOfWeekRow = document.getElementById('row_day_of_week');
|
||||
const specificDateRow = document.getElementById('row_specific_date');
|
||||
const isAvailableSelect = document.getElementById('is_available');
|
||||
const offReasonRow = document.getElementById('row_off_reason');
|
||||
|
||||
function toggleRuleTypeFields() {
|
||||
const selectedType = document.querySelector('input[name="rule_type"]:checked').value;
|
||||
dayOfWeekRow.style.display = (selectedType === 'weekly') ? '' : 'none';
|
||||
specificDateRow.style.display = (selectedType === 'specific') ? '' : 'none';
|
||||
}
|
||||
|
||||
function toggleOffReasonField() {
|
||||
offReasonRow.style.display = (isAvailableSelect.value === '0') ? '' : 'none';
|
||||
}
|
||||
|
||||
ruleTypeRadios.forEach(radio => radio.addEventListener('change', toggleRuleTypeFields));
|
||||
isAvailableSelect.addEventListener('change', toggleOffReasonField);
|
||||
|
||||
// 페이지 로드 시 초기 상태 설정
|
||||
toggleRuleTypeFields();
|
||||
toggleOffReasonField();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
$sub_menu = "800400";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '전문가 방문 관리';
|
||||
|
||||
// EstimateManager 클래스 로드
|
||||
require_once G5_PATH . '/adm/order_manage/classes/EstimateManager.class.php';
|
||||
$estimateManager = new EstimateManager();
|
||||
|
||||
// 액션 처리
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
$wr_id = (int) ($_REQUEST['wr_id'] ?? 0);
|
||||
|
||||
// POST 요청 처리
|
||||
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
try {
|
||||
if ($action === 'confirm_payment') {
|
||||
$result = $estimateManager->confirmExpertVisitPayment($wr_id);
|
||||
} elseif ($action === 'schedule_visit') {
|
||||
$visit_datetime = clean_xss_tags($_POST['visit_datetime'] ?? '');
|
||||
$expert_id = clean_xss_tags($_POST['expert_id'] ?? '');
|
||||
$result = $estimateManager->scheduleExpertVisit($wr_id, $visit_datetime, $expert_id);
|
||||
} elseif ($action === 'complete_visit') {
|
||||
$visit_notes = clean_xss_tags($_POST['visit_notes'] ?? '');
|
||||
$result = $estimateManager->completeExpertVisit($wr_id, $visit_notes);
|
||||
} else {
|
||||
throw new Exception('알 수 없는 요청입니다.');
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
alert($result['message'], './expert_visits.php');
|
||||
} else {
|
||||
throw new Exception($result['message']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert('오류: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
goto_url('./expert_visits.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
$sfl = isset($_GET['sfl']) ? clean_xss_tags($_GET['sfl']) : '';
|
||||
$stx = isset($_GET['stx']) ? clean_xss_tags($_GET['stx']) : '';
|
||||
$status_filter = isset($_GET['status_filter']) ? clean_xss_tags($_GET['status_filter']) : '';
|
||||
|
||||
$where = [];
|
||||
if ($stx) {
|
||||
if ($sfl === 'customer_name') {
|
||||
$where[] = "cm.mb_name LIKE '%{$stx}%'";
|
||||
} elseif ($sfl === 'expert_name') {
|
||||
$where[] = "em.mb_name LIKE '%{$stx}%'";
|
||||
}
|
||||
}
|
||||
if ($status_filter) {
|
||||
$where[] = "e.temp_2 = '{$status_filter}'";
|
||||
}
|
||||
|
||||
$where_clause = empty($where) ? "e.temp_1 = 'Y'" : "e.temp_1 = 'Y' AND " . implode(' AND ', $where);
|
||||
|
||||
// 페이징
|
||||
$page = (int) ($_GET['page'] ?? 1);
|
||||
$page_rows = 10;
|
||||
|
||||
$total_count_sql = "
|
||||
SELECT COUNT(*) as cnt
|
||||
FROM estimate e
|
||||
LEFT JOIN g5_write_order w ON e.wr_id = w.wr_id
|
||||
LEFT JOIN g5_member cm ON w.mb_id = cm.mb_id
|
||||
LEFT JOIN g5_member em ON e.extra_4 = em.mb_id
|
||||
WHERE {$where_clause}
|
||||
";
|
||||
$total_count_res = sql_fetch($total_count_sql);
|
||||
$total_count = $total_count_res['cnt'];
|
||||
$total_page = ceil($total_count / $page_rows);
|
||||
$from_record = ($page - 1) * $page_rows;
|
||||
|
||||
// 목록 조회
|
||||
$sql = "
|
||||
SELECT
|
||||
e.id as estimate_id, e.wr_id, e.temp_1, e.temp_2 as status, e.temp_3 as visit_fee,
|
||||
e.temp_4 as visit_datetime, e.temp_5 as visit_notes, e.extra_4 as expert_id,
|
||||
w.wr_subject, w.mb_id as customer_id,
|
||||
cm.mb_name as customer_name, cm.mb_hp as customer_phone,
|
||||
em.mb_name as expert_name
|
||||
FROM estimate e
|
||||
LEFT JOIN g5_write_order w ON e.wr_id = w.wr_id
|
||||
LEFT JOIN g5_member cm ON w.mb_id = cm.mb_id
|
||||
LEFT JOIN g5_member em ON e.extra_4 = em.mb_id
|
||||
WHERE {$where_clause}
|
||||
ORDER BY e.id DESC
|
||||
LIMIT {$from_record}, {$page_rows}
|
||||
";
|
||||
$result = sql_query($sql);
|
||||
$visits = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$visits[] = $row;
|
||||
}
|
||||
|
||||
// 전문가 목록 (레벨 8 이상)
|
||||
$experts = [];
|
||||
$expert_sql = "SELECT mb_id, mb_name FROM g5_member WHERE mb_level >= 9 ORDER BY mb_name";
|
||||
|
||||
$expert_result = sql_query($expert_sql);
|
||||
while ($row = sql_fetch_array($expert_result)) {
|
||||
$experts[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
// var_dump($expert_sql);
|
||||
// var_dump(sql_fetch_array($expert_result));
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
고객이 요청한 전문가 방문을 관리합니다. <br> 결제 확인, 일정 조율, 방문 완료 처리를 할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 검색 폼 -->
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<label for="sfl" class="sound_only">검색대상</label>
|
||||
<select name="sfl" id="sfl">
|
||||
<option value="customer_name" <?php echo get_selected($sfl, 'customer_name'); ?>>고객명</option>
|
||||
<option value="expert_name" <?php echo get_selected($sfl, 'expert_name'); ?>>전문가명</option>
|
||||
</select>
|
||||
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" class="frm_input">
|
||||
|
||||
<label for="status_filter" class="sound_only">상태</label>
|
||||
<select name="status_filter" id="status_filter">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="requested" <?php echo get_selected($status_filter, 'requested'); ?>>결제 대기</option>
|
||||
<option value="payment_confirmed" <?php echo get_selected($status_filter, 'payment_confirmed'); ?>>일정 조율</option>
|
||||
<option value="scheduled" <?php echo get_selected($status_filter, 'scheduled'); ?>>방문 예정</option>
|
||||
<option value="completed" <?php echo get_selected($status_filter, 'completed'); ?>>방문 완료</option>
|
||||
<option value="cancelled" <?php echo get_selected($status_filter, 'cancelled'); ?>>취소</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="검색" class="btn_submit">
|
||||
</form>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">견적 ID</th>
|
||||
<th scope="col">고객명</th>
|
||||
<th scope="col">연락처</th>
|
||||
<th scope="col">방문 상태</th>
|
||||
<th scope="col">방문 비용</th>
|
||||
<th scope="col">방문 일정</th>
|
||||
<th scope="col">담당 전문가</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($visits)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="empty_table">데이터가 없습니다.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($visits as $visit): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<?php echo G5_BBS_URL; ?>/board.php?bo_table=order&wr_id=<?php echo $visit['wr_id']; ?>"
|
||||
target="_blank"><?php echo $visit['wr_id']; ?></a>
|
||||
</td>
|
||||
<td><?php echo $visit['customer_name']; ?></td>
|
||||
<td><?php echo $visit['customer_phone']; ?></td>
|
||||
<td>
|
||||
<?php
|
||||
switch ($visit['status']) {
|
||||
case 'requested': echo '결제 대기'; break;
|
||||
case 'payment_confirmed': echo '일정 조율'; break;
|
||||
case 'scheduled': echo '방문 예정'; break;
|
||||
case 'completed': echo '방문 완료'; break;
|
||||
case 'cancelled': echo '취소'; break;
|
||||
default: echo '알 수 없음';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo number_format($visit['visit_fee']); ?>원</td>
|
||||
<td><?php echo $visit['visit_datetime'] ? date('Y-m-d H:i', strtotime($visit['visit_datetime'])) : '-'; ?></td>
|
||||
<td><?php echo $visit['expert_name'] ?: '미배정'; ?></td>
|
||||
<td class="td_mng td_mng_l">
|
||||
<?php if ($visit['status'] === 'requested'): ?>
|
||||
<form name="frm_confirm_<?php echo $visit['wr_id']; ?>" method="post" style="display:inline;">
|
||||
<input type="hidden" name="action" value="confirm_payment">
|
||||
<input type="hidden" name="wr_id" value="<?php echo $visit['wr_id']; ?>">
|
||||
<button type="submit" class="btn btn_03"
|
||||
onclick="return confirm('결제를 확인 처리하시겠습니까?');">결제확인</button>
|
||||
</form>
|
||||
<?php elseif ($visit['status'] === 'payment_confirmed'): ?>
|
||||
<button type="button" class="btn btn_02"
|
||||
onclick="openScheduleModal(<?php echo $visit['wr_id']; ?>)">일정조율</button>
|
||||
<?php elseif ($visit['status'] === 'scheduled'): ?>
|
||||
<form name="frm_complete_<?php echo $visit['wr_id']; ?>" method="post" style="display:inline;">
|
||||
<input type="hidden" name="action" value="complete_visit">
|
||||
<input type="hidden" name="wr_id" value="<?php echo $visit['wr_id']; ?>">
|
||||
<input type="hidden" name="visit_notes" value="방문 완료됨">
|
||||
<button type="submit" class="btn btn_01"
|
||||
onclick="return confirm('방문을 완료 처리하시겠습니까?');">방문완료</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$qstr&page="); ?>
|
||||
|
||||
<!-- 일정 조율 모달 -->
|
||||
<div id="scheduleModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>방문 일정 조율</h3>
|
||||
<span class="close" onclick="closeScheduleModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="frm_schedule" id="frm_schedule" method="post">
|
||||
<input type="hidden" name="action" value="schedule_visit">
|
||||
<input type="hidden" name="wr_id" id="modal_wr_id">
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_visit_datetime">방문 일시</label></th>
|
||||
<td>
|
||||
<input type="datetime-local" name="visit_datetime" id="modal_visit_datetime"
|
||||
class="frm_input" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="modal_expert_id">담당 전문가</label></th>
|
||||
<td>
|
||||
<select name="expert_id" id="modal_expert_id" class="frm_input">
|
||||
<option value="">선택하세요</option>
|
||||
<?php foreach ($experts as $expert): ?>
|
||||
<option value="<?php echo $expert['mb_id']; ?>">
|
||||
<?php echo $expert['mb_name']; ?> (<?php echo $expert['mb_id']; ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<button type="submit" class="btn_submit">일정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
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: #fefefe;
|
||||
margin: 10% auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 15px 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function openScheduleModal(wr_id) {
|
||||
document.getElementById('modal_wr_id').value = wr_id;
|
||||
document.getElementById('scheduleModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeScheduleModal() {
|
||||
document.getElementById('scheduleModal').style.display = 'none';
|
||||
}
|
||||
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById('scheduleModal');
|
||||
if (event.target === modal) {
|
||||
closeScheduleModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
die(json_encode(['success' => false, 'message' => '관리자 권한이 필요합니다.']));
|
||||
}
|
||||
|
||||
// EstimateManager 로드
|
||||
require_once G5_PATH . '/adm/order_manage/classes/EstimateManager.class.php';
|
||||
$estimate_manager = new EstimateManager();
|
||||
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'confirm_payment':
|
||||
$wr_id = (int) ($_POST['wr_id'] ?? 0);
|
||||
if (!$wr_id) {
|
||||
throw new Exception('견적 ID가 필요합니다.');
|
||||
}
|
||||
|
||||
$result = $estimate_manager->confirmExpertVisitPayment($wr_id, 'admin');
|
||||
|
||||
if (!$result['success']) {
|
||||
throw new Exception($result['message']);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '전문가 방문 비용 결제가 확인되었습니다.',
|
||||
'data' => $result['data']
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'schedule_visit':
|
||||
$wr_id = (int) ($_POST['wr_id'] ?? 0);
|
||||
$visit_datetime = clean_xss_tags($_POST['visit_datetime'] ?? '');
|
||||
$expert_id = clean_xss_tags($_POST['expert_id'] ?? '');
|
||||
|
||||
if (!$wr_id || !$visit_datetime) {
|
||||
throw new Exception('필수 정보가 누락되었습니다.');
|
||||
}
|
||||
|
||||
// 날짜 형식 변환 (datetime-local -> Y-m-d H:i)
|
||||
$visit_datetime = date('Y-m-d H:i', strtotime($visit_datetime));
|
||||
|
||||
$result = $estimate_manager->scheduleExpertVisit($wr_id, $visit_datetime, $expert_id, 'admin');
|
||||
|
||||
if (!$result['success']) {
|
||||
throw new Exception($result['message']);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '전문가 방문 일정이 설정되었습니다.',
|
||||
'data' => $result['data']
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'complete_visit':
|
||||
$wr_id = (int) ($_POST['wr_id'] ?? 0);
|
||||
$visit_notes = clean_xss_tags($_POST['visit_notes'] ?? '');
|
||||
|
||||
if (!$wr_id) {
|
||||
throw new Exception('견적 ID가 필요합니다.');
|
||||
}
|
||||
|
||||
$result = $estimate_manager->completeExpertVisit($wr_id, $visit_notes, 'admin');
|
||||
|
||||
if (!$result['success']) {
|
||||
throw new Exception($result['message']);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '전문가 방문이 완료 처리되었습니다.',
|
||||
'data' => $result['data']
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'get_visit_info':
|
||||
$wr_id = (int) ($_GET['wr_id'] ?? 0);
|
||||
if (!$wr_id) {
|
||||
throw new Exception('견적 ID가 필요합니다.');
|
||||
}
|
||||
|
||||
$visit_info = $estimate_manager->getExpertVisitInfo($wr_id);
|
||||
|
||||
if (!$visit_info) {
|
||||
throw new Exception('전문가 방문 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $visit_info
|
||||
]);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('유효하지 않은 액션입니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("expert_visits_ajax.php Error: " . $e->getMessage());
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
$sub_menu = '800900';
|
||||
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'] ?? '';
|
||||
|
||||
$sql_file_path = __DIR__ . '/install.sql';
|
||||
$tables_to_check = get_tables_from_sql_file($sql_file_path);
|
||||
|
||||
if ($action === 'install') {
|
||||
check_admin_token();
|
||||
try {
|
||||
$schemaManager = new SchemaManager($sql_file_path);
|
||||
$schemaManager->execute();
|
||||
$db_results = $schemaManager->get_results();
|
||||
$menu_msg = create_admin_menu_file();
|
||||
$install_result = ['db' => $db_results, '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.menu800.order_manage.php';
|
||||
if (file_exists($menu_file)) {
|
||||
if (@unlink($menu_file)) {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 성공';
|
||||
} else {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function create_admin_menu_file() {
|
||||
$source_file = __DIR__ . '/admin.menu800.order_manage.php';
|
||||
$target_file = G5_ADMIN_PATH . '/admin.menu800.order_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 = !empty($existing_tables); // 하나라도 테이블이 있으면 설치된 것으로 간주
|
||||
?>
|
||||
|
||||
<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 #4A90E2; }
|
||||
.install-header h1 { color: #4A90E2; 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: #f8f9fa; 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: #4A90E2; 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: #357ABD; color: white; }
|
||||
.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-file-text-o"></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 '메뉴 파일 : '.$install_result['menu']; ?></p>
|
||||
<p><a href="./config_manager.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="./config_manager.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></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($tables_to_check as $table): ?>
|
||||
<tr>
|
||||
<td><code><?php echo $table; ?></code></td>
|
||||
<td>
|
||||
<?php if (in_array($table, $existing_tables)): ?>
|
||||
<span class="status-ok"><i class="fa fa-check"></i> 설치됨</span>
|
||||
<?php else: ?>
|
||||
<span class="status-missing"><i class="fa fa-times"></i> 미설치</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if (!$is_installed): ?>
|
||||
<form method="post" onsubmit="return confirm('솔루션을 설치하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="install-btn"><i class="fa fa-download"></i> 솔루션 설치하기</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_installed && !$install_result && !$delete_result): ?>
|
||||
<div class="button-group" style="text-align: center; margin-top: 20px;">
|
||||
<form method="post" onsubmit="return confirm('기존 데이터는 유지되며, 변경된 DB 구조만 업데이트 됩니다. 진행하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn btn-secondary"><i class="fa fa-sync"></i> 재설치 (업데이트)</button>
|
||||
</form>
|
||||
<form method="post" onsubmit="return confirm('정말로 솔루션을 삭제하시겠습니까? 모든 관련 데이터와 파일이 영구적으로 삭제됩니다.');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn-danger"><i class="fa fa-trash"></i> 솔루션 삭제하기</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,547 @@
|
||||
-- 1. 견적서 마스터 테이블 (기존 유지)
|
||||
-- [최종안] 상담가, 수수료, 공통 관리 필드, 여분 필드 모두 포함
|
||||
CREATE TABLE IF NOT EXISTS `estimate`
|
||||
(
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`wr_id` int DEFAULT NULL COMMENT '원본 게시물 ID',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'requesting' COMMENT '견적 상태',
|
||||
`bidding_deadline` DATETIME DEFAULT NULL COMMENT '입찰 마감일시',
|
||||
`expert_id` VARCHAR(20) DEFAULT NULL COMMENT '배정된 전문가 ID',
|
||||
`expert_visit_status` VARCHAR(20) DEFAULT NULL COMMENT '전문가 방문 상태',
|
||||
`commission_fee` decimal(10, 2) DEFAULT '0.00' COMMENT '시스템 이용료(수수료)',
|
||||
`company_name` varchar(100) DEFAULT NULL COMMENT '업체명',
|
||||
`site_name` varchar(100) DEFAULT NULL COMMENT '사이트명',
|
||||
`estimate_date` varchar(30) DEFAULT NULL COMMENT '견적일',
|
||||
`house_type` varchar(50) DEFAULT NULL COMMENT '집의 유형',
|
||||
`house_size` varchar(50) DEFAULT NULL COMMENT '평형',
|
||||
`material` varchar(50) DEFAULT NULL COMMENT '창 재질',
|
||||
`color` varchar(20) DEFAULT NULL COMMENT '창호 색상',
|
||||
`glass_thickness` varchar(20) DEFAULT NULL COMMENT '유리두께',
|
||||
`install` varchar(5) DEFAULT NULL COMMENT '시공여부',
|
||||
`zip_code` varchar(10) DEFAULT NULL COMMENT '우편번호',
|
||||
`address1` varchar(255) DEFAULT NULL COMMENT '기본주소',
|
||||
`address2` varchar(255) DEFAULT NULL COMMENT '상세주소',
|
||||
`address3` varchar(255) DEFAULT NULL COMMENT '참고항목',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '전문가방문요청여부(Y/N)',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '전문가방문상태(requested/scheduled/completed/cancelled)',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '전문가방문비용',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '전문가방문일정(YYYY-MM-DD HH:MM)',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '전문가방문메모',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '고객연락처(전문가방문용)',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '결제상태(pending/paid/cancelled)',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '추가요청사항',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '관리자메모',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '예비필드',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `wr_id` (`wr_id`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='견적서 마스터 정보';
|
||||
|
||||
-- 2. 견적서 상세 항목 테이블 (브랜드 필드 강화)
|
||||
-- [최종안] order.php의 템플릿 필드와 일치하도록 컬럼 및 코멘트 수정
|
||||
CREATE TABLE IF NOT EXISTS `estimate_item`
|
||||
(
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`estimate_id` int DEFAULT NULL COMMENT '견적서 ID (estimate.id)',
|
||||
`dealer_id` VARCHAR(255) NULL DEFAULT NULL COMMENT '[대리점] 제안 회원 ID',
|
||||
`no` int DEFAULT NULL COMMENT '항목 번호',
|
||||
`location` varchar(50) DEFAULT NULL COMMENT '창 종류 (거실, 안방 등)',
|
||||
`spec_width` varchar(20) DEFAULT NULL COMMENT '기존창 규격 (가로)',
|
||||
`spec_height` varchar(20) DEFAULT NULL COMMENT '기존창 규격 (세로)',
|
||||
`window_main_type` VARCHAR(50) DEFAULT NULL COMMENT '창호 형태 (1차: 일반창/프로젝트창)',
|
||||
`windowType` varchar(20) DEFAULT NULL COMMENT '창호 형태 (2차: 단창/이중창)',
|
||||
`windowRatio` varchar(20) DEFAULT NULL COMMENT '창 비율 (2W, 4W 등)',
|
||||
`glass_color` varchar(50) DEFAULT NULL COMMENT '유리사양 (색상)',
|
||||
`handle` varchar(20) DEFAULT NULL COMMENT '시정장치 (핸들)',
|
||||
`replacePart` varchar(20) DEFAULT NULL COMMENT '교체 위치 (외부/내부)',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '비고 (기타 특이사항)',
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- 아래는 대리점이 입력하는 필드입니다.
|
||||
-- ----------------------------------------------------
|
||||
`product` varchar(50) DEFAULT NULL COMMENT '[대리점] 품명',
|
||||
`qty` int DEFAULT NULL COMMENT '[대리점] 수량',
|
||||
`price` int DEFAULT NULL COMMENT '[대리점] 단가',
|
||||
`amount` int DEFAULT NULL COMMENT '[대리점] 금액',
|
||||
`brand` varchar(30) DEFAULT NULL COMMENT '[대리점] 창호브랜드명',
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- 시스템 관리용 필드
|
||||
-- ----------------------------------------------------
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부 (1:사용, 0:미사용)',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- 사용하지 않지만 데이터 보존을 위해 남겨둔 필드
|
||||
-- ----------------------------------------------------
|
||||
`color` varchar(20) DEFAULT NULL COMMENT '[구] 색상',
|
||||
`glass_thickness` varchar(20) DEFAULT NULL COMMENT '[구] 유리두께',
|
||||
`screen` varchar(10) DEFAULT NULL COMMENT '[구] 방충망',
|
||||
`door_dir` varchar(20) DEFAULT NULL COMMENT '[구] 문방향',
|
||||
`install` varchar(5) DEFAULT NULL 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_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',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `estimate_id` (`estimate_id`),
|
||||
CONSTRAINT `fk_item_to_estimate` FOREIGN KEY (`estimate_id`) REFERENCES `estimate` (`id`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='견적서 상세 항목';
|
||||
|
||||
-- 3. 견적 및 항목 변경 이력 테이블 (기존 유지)
|
||||
-- [install_backup.sql 내용 + 공통 관리 필드, 여분 필드 추가]
|
||||
CREATE TABLE IF NOT EXISTS `estimate_history`
|
||||
(
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '이력 고유 ID',
|
||||
`estimate_id` int NOT NULL COMMENT '견적서 ID',
|
||||
`item_id` int DEFAULT NULL COMMENT '견적 항목 ID (항목 변경 시)',
|
||||
`action` varchar(20) NOT NULL COMMENT '작업 종류 (create, update, delete, status_change)',
|
||||
`change_details` longtext COMMENT '변경된 데이터 (JSON 형식)',
|
||||
`changed_by` varchar(20) NOT NULL COMMENT '변경자 ID',
|
||||
`changed_at` datetime NOT NULL 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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `estimate_id` (`estimate_id`),
|
||||
KEY `item_id` (`item_id`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='견적 및 항목 변경 이력';
|
||||
|
||||
-- 4. 대리점 입찰 정보 테이블 (기존 유지)
|
||||
-- [wr_id 컬럼 추가 + 공통 관리 필드, 여분 필드 추가]
|
||||
CREATE TABLE IF NOT EXISTS `estimate_bidding`
|
||||
(
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`estimate_id` int NOT NULL COMMENT '견적서 ID',
|
||||
`wr_id` int DEFAULT NULL COMMENT '게시판 답변글 ID',
|
||||
`dealer_id` varchar(20) NOT NULL COMMENT '입찰한 대리점 회원 ID',
|
||||
`bid_amount` int NOT NULL COMMENT '대리점 제시 금액',
|
||||
`bid_message` text COMMENT '대리점 메모',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'bidding' COMMENT '입찰 상태 (bidding, selected, unselected)',
|
||||
`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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `estimate_id` (`estimate_id`),
|
||||
KEY `dealer_id` (`dealer_id`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='대리점 입찰 정보';
|
||||
|
||||
-- 5. ⭐ '전문가 방문 요청'으로 변경: 전문가 방문 예약 테이블
|
||||
CREATE TABLE IF NOT EXISTS `expert_visit_reservations`
|
||||
(
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '예약 고유번호',
|
||||
`wr_id` int DEFAULT NULL COMMENT '연관 게시물 ID',
|
||||
`customer_id` varchar(20) DEFAULT NULL COMMENT '고객 회원 ID',
|
||||
`customer_name` varchar(50) DEFAULT NULL COMMENT '고객명',
|
||||
`customer_phone` varchar(20) DEFAULT NULL COMMENT '고객 연락처',
|
||||
`customer_email` varchar(100) DEFAULT NULL COMMENT '고객 이메일',
|
||||
`visit_date` DATE DEFAULT NULL COMMENT '방문 날짜',
|
||||
`visit_time` TIME DEFAULT NULL COMMENT '방문 시간',
|
||||
`expert_id` varchar(20) DEFAULT NULL COMMENT '배정된 전문가 ID',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'payment_pending' COMMENT '예약 상태 (payment_pending/reserved/completed/cancelled)',
|
||||
`payment_amount` int DEFAULT '0' COMMENT '방문 비용',
|
||||
`payment_status` varchar(20) DEFAULT 'pending' COMMENT '결제 상태 (pending/paid/cancelled)',
|
||||
`request_memo` text DEFAULT NULL COMMENT '고객 요청사항',
|
||||
`admin_memo` text DEFAULT NULL COMMENT '관리자 메모',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '방문시간구간(예:09:00-11:00)',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '방문종류(현장방문/전화상담)',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '방문지역',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '방문예상시간(분)',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '추가정보',
|
||||
`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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `customer_id` (`customer_id`),
|
||||
KEY `visit_date` (`visit_date`),
|
||||
KEY `visit_time` (`visit_time`),
|
||||
KEY `expert_id` (`expert_id`),
|
||||
KEY `status` (`status`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='전문가 방문 예약 정보';
|
||||
|
||||
-- 6. ⭐ '전문가 방문 요청'으로 변경: 전문가 방문 가능 시간 설정 테이블
|
||||
CREATE TABLE IF NOT EXISTS `expert_visit_schedules`
|
||||
(
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '일정 고유번호',
|
||||
`expert_id` varchar(20) DEFAULT NULL COMMENT '전문가 ID (NULL시 전체 설정)',
|
||||
`day_of_week` int DEFAULT NULL COMMENT '요일 (1:월요일~7:일요일, NULL시 특정날짜)',
|
||||
`specific_date` DATE DEFAULT NULL COMMENT '특정 날짜 (요일 설정과 배타적)',
|
||||
`start_time` TIME NOT NULL COMMENT '방문 시작 시간',
|
||||
`end_time` TIME NOT NULL COMMENT '방문 종료 시간',
|
||||
`time_slot` int NOT NULL DEFAULT '60' COMMENT '예약 단위 시간(분)',
|
||||
`max_persons` int NOT NULL DEFAULT '1' COMMENT '동시간대 최대 예약 가능 인원',
|
||||
`is_available` tinyint(1) NOT NULL DEFAULT '1' COMMENT '예약 가능 여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '방문종류제한',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '휴무사유',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '특별일정명',
|
||||
`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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `expert_id` (`expert_id`),
|
||||
KEY `day_of_week` (`day_of_week`),
|
||||
KEY `specific_date` (`specific_date`),
|
||||
UNIQUE KEY `unique_schedule` (`expert_id`, `day_of_week`, `specific_date`, `start_time`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='전문가 방문 가능 시간 설정';
|
||||
|
||||
-- 7. ⭐ 새로 추가: 창호 브랜드 마스터 테이블
|
||||
CREATE TABLE IF NOT EXISTS `order_window_brands`
|
||||
(
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '브랜드 고유번호',
|
||||
`brand_name` varchar(50) NOT NULL COMMENT '브랜드명',
|
||||
`brand_code` varchar(20) DEFAULT NULL COMMENT '브랜드 코드',
|
||||
`manufacturer` varchar(100) DEFAULT NULL COMMENT '제조사',
|
||||
`description` text DEFAULT NULL COMMENT '브랜드 설명',
|
||||
`logo_url` varchar(255) DEFAULT NULL COMMENT '로고 이미지 URL',
|
||||
`website_url` varchar(255) DEFAULT NULL COMMENT '홈페이지 URL',
|
||||
`sort_order` int DEFAULT '0' COMMENT '정렬 순서',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '브랜드등급(프리미엄/일반/보급)',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '주력제품군',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '가격대',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT 'A/S기간',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '보증기간',
|
||||
`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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `brand_name` (`brand_name`),
|
||||
KEY `brand_code` (`brand_code`),
|
||||
KEY `sort_order` (`sort_order`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT ='창호 브랜드 마스터';
|
||||
|
||||
-- 기존 테이블들 유지 (order_config, order_mail_templates, order_sms_templates)
|
||||
-- 기존 테이블 활용 최소 변경 SQL
|
||||
-- 기존 temp_, extra_ 컬럼을 활용하여 새 기능 구현
|
||||
|
||||
-- 1. 시스템 설정을 위한 간단한 테이블 (기존 테이블 없을 경우만 생성)
|
||||
CREATE TABLE IF NOT EXISTS `order_config` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`config_key` varchar(100) NOT NULL,
|
||||
`config_value` text NOT NULL,
|
||||
`config_desc` varchar(255) DEFAULT NULL,
|
||||
`config_type` varchar(20) DEFAULT 'text',
|
||||
`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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `config_key` (`config_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='시스템 설정';
|
||||
|
||||
-- 2. 메일 템플릿 테이블 (기존 테이블 없을 경우만 생성)
|
||||
CREATE TABLE IF NOT EXISTS `order_mail_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_name` varchar(255) NOT NULL,
|
||||
`subject` varchar(255) NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`variables` text DEFAULT NULL COMMENT '사용 가능한 변수들 (JSON)',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '발송대상그룹',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '발송조건',
|
||||
`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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `template_key` (`template_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='메일 템플릿';
|
||||
|
||||
-- 3. SMS 템플릿 테이블 (기존 테이블 없을 경우만 생성)
|
||||
CREATE TABLE IF NOT EXISTS `order_sms_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_name` varchar(255) NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`variables` text DEFAULT NULL COMMENT '사용 가능한 변수들 (JSON)',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '발송대상그룹',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '발송조건',
|
||||
`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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `template_key` (`template_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='SMS 템플릿';
|
||||
|
||||
-- ⭐ 기본 설정값 (중복 방지)
|
||||
INSERT IGNORE INTO `order_config` (`config_key`, `config_value`, `config_desc`, `config_type`) VALUES
|
||||
('timer_enabled', '1', '24시간 타이머 활성화 여부', 'boolean'),
|
||||
('timer_message_active', '견적 제안 마감까지 {time} 남았습니다.', '타이머 활성화 시 메시지', 'text'),
|
||||
('timer_message_inactive', '고객 작성이 완료되었습니다. 24시간 후에 확인 바랍니다.', '타이머 비활성화 시 메시지', 'text'),
|
||||
('contract_deposit_rate', '10', '계약금 비율 (%)', 'number'),
|
||||
('middle_payment_rate', '40', '중도금 비율 (%)', 'number'),
|
||||
('expert_visit_fee', '50000', '전문가 방문 비용', 'number'),
|
||||
('expert_account_info', '국민은행 123-456-789 (주)창호전문가', '전문가 방문 계좌 정보', 'text'),
|
||||
('notification_mode', 'log', '알림 발송 모드 (log/send)', 'select'),
|
||||
('dealer_commission_level_5', '5', '레벨 5 대리점 수수료율 (%)', 'number'),
|
||||
('dealer_commission_level_6', '11', '레벨 6 대리점 수수료율 (%)', 'number'),
|
||||
('dealer_commission_level_7', '19', '레벨 7 대리점 수수료율 (%)', 'number'),
|
||||
('price_increment_step', '1000', '견적서 단가 입력 증감 단위', 'number'),
|
||||
('payment_account_info', '국민은행 123-456-789012 (주)창호전문', '결제 계좌 정보', 'text'),
|
||||
('status_견적신청중', '수정중', '상태명: 견적신청중', 'text'),
|
||||
('status_작성완료', '작성완료', '상태명: 작성완료', 'text'),
|
||||
('status_계약금입금예정', '계약금입금예정', '상태명: 계약금입금예정', 'text'),
|
||||
('status_계약금입금완료', '계약금입금완료', '상태명: 계약금입금완료', 'text'),
|
||||
('status_중도금입금예정', '중도금입금예정', '상태명: 중도금입금예정', 'text'),
|
||||
('status_중도금입금완료', '중도금입금완료', '상태명: 중도금입금완료', 'text'),
|
||||
('status_잔금입금예정', '잔금입금예정', '상태명: 잔금입금예정', 'text'),
|
||||
('status_잔금처리완료', '잔금처리완료', '상태명: 잔금처리완료', 'text'),
|
||||
('status_시공완료', '시공완료', '상태명: 시공완료', 'text'),
|
||||
('status_견적제안', '견적제안', '상태명: 견적제안', 'text'),
|
||||
('status_견적채택', '견적채택', '상태명: 견적채택', 'text'),
|
||||
('status_견적취소', '견적취소', '상태명: 견적취소', 'text'),
|
||||
('expert_visit_default_start', '09:00', '기본 방문 시작 시간', 'time'),
|
||||
('expert_visit_default_end', '18:00', '기본 방문 종료 시간', 'time'),
|
||||
('expert_visit_default_slot', '60', '기본 예약 단위 시간(분)', 'number'),
|
||||
('expert_visit_default_max', '2', '기본 동시간대 최대 예약 인원', 'number'),
|
||||
('expert_visit_account_info', '국민은행 123-456-789 (주)창호방문', '방문비 입금 계좌', 'text'),
|
||||
('reservation_status_payment_pending', '입금예정', '예약상태: 입금예정', 'text'),
|
||||
('reservation_status_reserved', '예약완료', '예약상태: 예약완료', 'text'),
|
||||
('reservation_status_completed', '방문완료', '예약상태: 방문완료', 'text'),
|
||||
('reservation_status_cancelled', '예약취소', '예약상태: 예약취소', 'text');
|
||||
|
||||
-- ⭐ 기본 메일 템플릿 (중복 방지)
|
||||
INSERT IGNORE INTO `order_mail_templates` (`template_key`, `template_name`, `subject`, `content`, `variables`) VALUES
|
||||
('customer_request_complete', '고객 - 견적 요청 완료', '견적 요청이 완료되었습니다', '안녕하세요 {customer_name}님,<br><br>견적 요청이 성공적으로 완료되었습니다.<br>24시간 후에 견적서를 확인해주시기 바랍니다.<br><br>감사합니다.', '["customer_name"]'),
|
||||
('agent_new_request', '대리점 - 새 견적 요청', '새로운 견적 요청이 등록되었습니다', '안녕하세요 {agent_name}님,<br><br>{customer_name}님이 견적을 요청했습니다.<br><br>제목: {request_title}<br>요청일: {request_date}<br><br>견적 작성 URL: {write_url}<br><br>24시간 내에 견적을 제안해주시기 바랍니다.', '["agent_name", "customer_name", "request_title", "request_date", "write_url"]'),
|
||||
('customer_quote_selected', '고객 - 견적 선택 완료', '견적이 선택되었습니다', '안녕하세요 {customer_name}님,<br><br>{agent_name} 대리점의 견적이 선택되었습니다.<br><br>계약금({deposit_amount}원)을 입금해주시기 바랍니다.<br><br>계좌정보: {account_info}', '["customer_name", "agent_name", "deposit_amount", "account_info"]'),
|
||||
('agent_quote_selected', '대리점 - 견적 선택됨', '견적이 선택되었습니다', '안녕하세요 {agent_name}님,<br><br>{customer_name}님이 귀하의 견적을 선택하셨습니다.<br><br>시공 일정을 관리자에게 알려주시기 바랍니다.<br><br>감사합니다.', '["agent_name", "customer_name"]'),
|
||||
('expert_visit_request', '전문가 방문 요청', '전문가 방문이 요청되었습니다', '안녕하세요 {customer_name}님,<br><br>전문가 방문이 요청되었습니다.<br><br>방문 비용: {visit_fee}원<br>계좌정보: {account_info}<br><br>입금 확인 후 방문 일정을 조율하겠습니다.', '["customer_name", "visit_fee", "account_info"]'),
|
||||
('bid_selected_email', '대리점 - 견적 선택 알림', '축하합니다! 견적이 선택되었습니다', '안녕하세요 {dealer_name}님,<br><br>축하합니다! 고객이 귀하의 견적을 선택하였습니다.<br><br><strong>견적 정보:</strong><br>- 견적번호: {estimate_id}<br>- 고객명: {customer_name}<br>- 견적금액: {bid_amount}원<br>- 선택일시: {selected_date}<br><br>이제 시공 일정을 조율해주시기 바랍니다.<br><br><a href="{estimate_url}">견적 상세보기</a><br><br>감사합니다.', '["dealer_name", "customer_name", "estimate_id", "bid_amount", "selected_date", "estimate_url"]'),
|
||||
('schedule_reminder_email', '대리점 - 시공 일정 알림', '시공 일정 안내', '안녕하세요 {dealer_name}님,<br><br>시공 일정을 안내드립니다.<br><br><strong>시공 정보:</strong><br>- 견적번호: {estimate_id}<br>- 고객명: {customer_name}<br>- 시공예정일: {construction_date}<br>- 중도금입금예정일: {interim_payment_date}<br><br>시공 준비를 완료해주시기 바랍니다.<br><br>감사합니다.', '["dealer_name", "customer_name", "estimate_id", "construction_date", "interim_payment_date"]'),
|
||||
('interim_due_email', '대리점 - 중도금 입금 기한 알림', '중도금 입금 기한 안내', '안녕하세요 {dealer_name}님,<br><br>중도금 입금 기한이 도래했습니다.<br><br><strong>시공 정보:</strong><br>- 견적번호: {estimate_id}<br>- 고객명: {customer_name}<br>- 시공예정일: {construction_date}<br>- 중도금입금예정일: {interim_payment_date}<br>- 남은 일수: {days_remaining}일<br><br>고객에게 중도금 입금을 안내해주시기 바랍니다.<br><br>감사합니다.', '["dealer_name", "customer_name", "estimate_id", "construction_date", "interim_payment_date", "days_remaining"]'),
|
||||
('expert_visit_payment_confirmed', '고객 - 전문가 방문 결제 확인', '전문가 방문 비용 결제가 확인되었습니다', '안녕하세요 {customer_name}님,<br><br>전문가 방문 비용 결제가 확인되었습니다.<br><br>곧 담당자가 연락드려 방문 일정을 조율하겠습니다.<br><br>감사합니다.', '["customer_name"]'),
|
||||
('expert_visit_scheduled', '고객 - 전문가 방문 일정 안내', '전문가 방문 일정이 확정되었습니다', '안녕하세요 {customer_name}님,<br><br>전문가 방문 일정이 확정되었습니다.<br><br><strong>방문 일정:</strong><br>- 방문 일시: {visit_datetime}<br>- 담당 전문가: {expert_name}<br><br>방문 시간에 맞춰 준비해주시기 바랍니다.<br><br>감사합니다.', '["customer_name", "visit_datetime", "expert_name"]'),
|
||||
('expert_visit_completed', '고객 - 전문가 방문 완료', '전문가 방문이 완료되었습니다', '안녕하세요 {customer_name}님,<br><br>전문가 방문이 완료되었습니다.<br><br>방문 결과를 바탕으로 정확한 견적을 제공해드리겠습니다.<br><br>감사합니다.', '["customer_name"]'),
|
||||
('expert_visit_request_new', '고객 - 전문가 방문 신청', '전문가 방문이 신청되었습니다', '안녕하세요 {customer_name}님,<br><br>전문가 방문이 신청되었습니다.<br><br><strong>예약 정보:</strong><br>- 예약번호: {reservation_id}<br>- 방문예정일: {visit_date} {visit_time}<br>- 방문비용: {expert_visit_fee}원<br><br>아래 계좌로 방문비용을 입금해주시면 예약이 확정됩니다.<br><br><strong>계좌 정보:</strong><br>{account_info}<br><br>감사합니다.', '["customer_name", "reservation_id", "visit_date", "visit_time", "expert_visit_fee", "account_info"]'),
|
||||
('expert_visit_confirmed', '고객 - 전문가 방문 확정', '전문가 방문이 확정되었습니다', '안녕하세요 {customer_name}님,<br><br>전문가 방문이 확정되었습니다.<br><br><strong>예약 정보:</strong><br>- 예약번호: {reservation_id}<br>- 방문일시: {visit_date} {visit_time}<br>- 담당 전문가: {expert_name}<br><br>예약 시간에 맞춰 준비해주시기 바랍니다.<br><br>감사합니다.', '["customer_name", "reservation_id", "visit_date", "visit_time", "expert_name"]'),
|
||||
('admin_new_visit_request', '관리자 - 새 전문가 방문 요청', '새로운 전문가 방문 요청이 신청되었습니다', '새로운 전문가 방문 요청이 신청되었습니다.<br><br><strong>예약 정보:</strong><br>- 고객명: {customer_name}<br>- 방문예정일: {visit_date} {visit_time}<br>- 연락처: {customer_phone}<br>- 요청사항: {request_memo}<br><br>입금 확인 후 예약을 확정해주세요.', '["customer_name", "visit_date", "visit_time", "customer_phone", "request_memo"]'),
|
||||
('payment_deposit_complete_customer', '고객 - 계약금 입금 완료', '계약금 입금이 확인되었습니다', '안녕하세요 {customer_name}님,<br><br>요청하신 견적의 계약금({deposit_amount}) 입금이 확인되었습니다.<br>담당 대리점에서 곧 시공 일정을 조율할 예정입니다.<br><br>감사합니다.', '["customer_name", "deposit_amount"]'),
|
||||
('payment_interim_due_customer', '고객 - 중도금 입금 안내', '중도금 입금 안내', '안녕하세요 {customer_name}님,<br><br>시공일({construction_date})이 확정됨에 따라 중도금 입금을 안내드립니다.<br>시공 2일 전까지 중도금({interim_amount})을 아래 계좌로 입금해주시기 바랍니다.<br><br>계좌정보: {account_info}<br><br>감사합니다.', '["customer_name", "construction_date", "interim_amount", "account_info"]'),
|
||||
('payment_interim_complete_customer', '고객 - 중도금 입금 완료', '중도금 입금이 확인되었습니다', '안녕하세요 {customer_name}님,<br><br>중도금({interim_amount}) 입금이 확인되었습니다.<br>예정된 날짜에 시공이 원활히 진행되도록 준비하겠습니다.<br><br>감사합니다.', '["customer_name", "interim_amount"]'),
|
||||
('construction_complete_customer', '고객 - 시공 완료', '시공이 완료되었습니다', '안녕하세요 {customer_name}님,<br><br>요청하신 견적의 시공이 완료되었습니다.<br>이용해주셔서 감사합니다.', '["customer_name"]'),
|
||||
('construction_complete_agent', '대리점 - 시공 완료', '{customer_name} 고객님의 시공이 완료 처리되었습니다', '안녕하세요 {agent_name}님,<br><br>{customer_name} 고객님의 시공이 완료 처리되었습니다.<br>수고하셨습니다.', '["agent_name", "customer_name"]'),
|
||||
('expert_new_assignment', '전문가 - 신규 방문 배정', '새로운 방문 예약이 배정되었습니다', '안녕하세요 {expert_name}님,<br><br>새로운 방문 예약이 배정되었습니다.<br><br><strong>예약 정보:</strong><br>- 고객명: {customer_name}<br>- 방문일시: {visit_date} {visit_time}<br><br>관리자 페이지에서 상세 내용을 확인해주세요.', '["expert_name", "customer_name", "visit_date", "visit_time"]');
|
||||
|
||||
-- ⭐ 기본 SMS 템플릿 (중복 방지)
|
||||
INSERT IGNORE INTO `order_sms_templates` (`template_key`, `template_name`, `content`, `variables`) VALUES
|
||||
('customer_request_complete', '고객 - 견적 요청 완료', '{customer_name}님, 견적 요청이 완료되었습니다. 24시간 후에 확인해주세요.', '["customer_name"]'),
|
||||
('agent_new_request', '대리점 - 새 견적 요청', '{agent_name}님, {customer_name}님이 견적을 요청했습니다. 24시간 내에 제안해주세요. {write_url}', '["agent_name", "customer_name", "write_url"]'),
|
||||
('customer_quote_selected', '고객 - 견적 선택 완료', '{customer_name}님, 견적이 선택되었습니다. 계약금 {deposit_amount}원을 입금해주세요.', '["customer_name", "deposit_amount"]'),
|
||||
('agent_quote_selected', '대리점 - 견적 선택됨', '{agent_name}님, {customer_name}님이 귀하의 견적을 선택하셨습니다. 시공 일정을 알려주세요.', '["agent_name", "customer_name"]'),
|
||||
('expert_visit_request', '전문가 방문 요청', '{customer_name}님, 전문가 방문이 요청되었습니다. 방문비 {visit_fee}원 입금 후 일정 조율하겠습니다.', '["customer_name", "visit_fee"]'),
|
||||
('bid_selected_sms', '대리점 - 견적 선택 알림', '{dealer_name}님, 축하합니다! {customer_name}님이 귀하의 견적({bid_amount}원)을 선택하셨습니다. 시공 일정을 조율해주세요.', '["dealer_name", "customer_name", "bid_amount"]'),
|
||||
('schedule_reminder_sms', '대리점 - 시공 일정 알림', '{dealer_name}님, {customer_name}님 시공예정일({construction_date}) 안내드립니다. 시공 준비를 완료해주세요.', '["dealer_name", "customer_name", "construction_date"]'),
|
||||
('interim_due_sms', '대리점 - 중도금 입금 기한 알림', '{dealer_name}님, {customer_name}님 중도금 입금기한({interim_payment_date})이 {days_remaining}일 남았습니다. 고객에게 안내해주세요.', '["dealer_name", "customer_name", "interim_payment_date", "days_remaining"]'),
|
||||
('expert_visit_payment_confirmed', '고객 - 전문가 방문 결제 확인', '{customer_name}님, 전문가 방문 비용 결제가 확인되었습니다. 곧 방문 일정을 안내드리겠습니다.', '["customer_name"]'),
|
||||
('expert_visit_scheduled', '고객 - 전문가 방문 일정 안내', '{customer_name}님, 전문가 방문 일정이 {visit_datetime}로 확정되었습니다.', '["customer_name", "visit_datetime"]'),
|
||||
('expert_visit_completed', '고객 - 전문가 방문 완료', '{customer_name}님, 전문가 방문이 완료되었습니다. 감사합니다.', '["customer_name"]'),
|
||||
('expert_visit_request_new', '고객 - 전문가 방문 신청', '{customer_name}님, 전문가 방문({visit_date} {visit_time})이 신청되었습니다. 방문비 {expert_visit_fee}원을 입금해주세요.', '["customer_name", "visit_date", "visit_time", "expert_visit_fee"]'),
|
||||
('expert_visit_confirmed', '고객 - 전문가 방문 확정', '{customer_name}님, 전문가 방문({visit_date} {visit_time})이 확정되었습니다. 시간에 맞춰 준비해주세요.', '["customer_name", "visit_date", "visit_time"]'),
|
||||
('admin_new_visit_request', '관리자 - 새 전문가 방문 요청', '새 전문가 방문 요청: {customer_name}님 {visit_date} {visit_time} (연락처: {customer_phone})', '["customer_name", "visit_date", "visit_time", "customer_phone"]'),
|
||||
('payment_deposit_complete_customer', '고객 - 계약금 입금 완료', '[{site_name}] {customer_name}님, 계약금 입금이 확인되었습니다. 곧 시공 일정이 안내됩니다.', '["site_name", "customer_name"]'),
|
||||
('payment_interim_due_customer', '고객 - 중도금 입금 안내', '[{site_name}] {customer_name}님, 중도금({interim_amount}) 입금 예정일({interim_payment_date})입니다. 입금 부탁드립니다.', '["site_name", "customer_name", "interim_amount", "interim_payment_date"]'),
|
||||
('payment_interim_complete_customer', '고객 - 중도금 입금 완료', '[{site_name}] {customer_name}님, 중도금 입금이 확인되었습니다. 시공을 준비하겠습니다.', '["site_name", "customer_name"]'),
|
||||
('construction_complete_customer', '고객 - 시공 완료', '[{site_name}] {customer_name}님, 요청하신 시공이 완료되었습니다. 감사합니다.', '["site_name", "customer_name"]'),
|
||||
('construction_complete_agent', '대리점 - 시공 완료', '[{site_name}] {agent_name}님, {customer_name} 고객님의 시공이 완료 처리되었습니다.', '["site_name", "agent_name", "customer_name"]'),
|
||||
('expert_new_assignment', '전문가 - 신규 방문 배정', '[{site_name}] {expert_name}님, {customer_name} 고객님의 방문 예약({visit_date} {visit_time})이 배정되었습니다.', '["site_name", "expert_name", "customer_name", "visit_date", "visit_time"]');
|
||||
|
||||
-- ⭐ 기본 창호 브랜드 데이터 삽입 (중복 방지)
|
||||
INSERT IGNORE INTO `order_window_brands` (`brand_name`, `brand_code`, `manufacturer`, `description`, `sort_order`, `temp_1`, `temp_2`) VALUES
|
||||
('KCC창호', 'KCC', 'KCC', '국내 대표 창호 브랜드', 1, '프리미엄', '고급창호'),
|
||||
('LG하우시스', 'LG', 'LG하우시스', 'LG 계열 창호 전문 브랜드', 2, '프리미엄', '시스템창호'),
|
||||
('현대L&C', 'HD', '현대L&C', '현대 계열 리빙 솔루션', 3, '프리미엄', '아파트창호'),
|
||||
('롯데창호', 'LOTTE', '롯데창호', '롯데 계열 창호 브랜드', 4, '일반', '일반창호'),
|
||||
('삼성창호', 'SAMSUNG', '삼성창호', '삼성 계열 창호 브랜드', 5, '일반', '시스템창호'),
|
||||
('대우창호', 'DAEWOO', '대우창호', '대우 계열 창호 브랜드', 6, '일반', '아파트창호'),
|
||||
('이녹스', 'INOX', '이녹스', '스테인리스 전문 창호', 7, '일반', '스테인리스'),
|
||||
('센텍창호', 'CENTEC', '센텍', '센텍 창호 시스템', 8, '보급', '일반창호'),
|
||||
('알루미늄창호', 'ALUMI', '기타', '일반 알루미늄 창호', 9, '보급', '알루미늄'),
|
||||
('기타', 'ETC', '기타', '기타 브랜드', 10, '보급', '기타');
|
||||
|
||||
-- -- ⭐ 기본 전문가 방문 스케줄 설정 (중복 방지)
|
||||
INSERT IGNORE INTO `expert_visit_schedules` (`day_of_week`, `start_time`, `end_time`, `time_slot`, `max_persons`, `temp_1`) VALUES
|
||||
(1, '09:00', '18:00', 60, 2, '현장방문'),
|
||||
(2, '09:00', '18:00', 60, 2, '현장방문'),
|
||||
(3, '09:00', '18:00', 60, 2, '현장방문'),
|
||||
(4, '09:00', '18:00', 60, 2, '현장방문'),
|
||||
(5, '09:00', '18:00', 60, 2, '현장방문');
|
||||
|
||||
-- 테이블 컬럼 활용 현황 주석 업데이트
|
||||
/*
|
||||
=== 📋 테이블별 temp_/extra_ 컬럼 사용 현황 ===
|
||||
|
||||
🔹 estimate 테이블:
|
||||
- temp_1: 전문가방문요청여부(Y/N) ✅사용중
|
||||
- temp_2: 전문가방문상태(requested/scheduled/completed/cancelled) ✅사용중
|
||||
- temp_3: 전문가방문비용 ✅사용중
|
||||
- temp_4: 전문가방문일정(YYYY-MM-DD HH:MM) ✅사용중
|
||||
- temp_5: 전문가방문메모 ✅사용중
|
||||
- extra_1: 고객연락처(전문가방문용) ✅사용중
|
||||
- extra_2: 결제상태(pending/paid/cancelled) ✅사용중
|
||||
- extra_3: 추가요청사항 ✅사용중
|
||||
- extra_4: 관리자메모 ✅사용중
|
||||
- extra_5: 예비필드 ⭕미사용
|
||||
|
||||
🔹 estimate_item 테이블:
|
||||
- temp_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
- extra_2~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 estimate_history 테이블:
|
||||
- temp_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 estimate_bidding 테이블:
|
||||
- temp_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 expert_visit_reservations 테이블: (신규)
|
||||
- temp_1: 방문시간구간(예:09:00-11:00) ✅사용중
|
||||
- temp_2: 방문종류(현장방문/전화상담) ✅사용중
|
||||
- temp_3: 방문지역 ✅사용중
|
||||
- temp_4: 방문예상시간(분) ✅사용중
|
||||
- temp_5: 추가정보 ✅사용중
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 expert_visit_schedules 테이블: (신규)
|
||||
- temp_1: 방문종류제한 ✅사용중
|
||||
- temp_2: 휴무사유 ✅사용중
|
||||
- temp_3: 특별일정명 ✅사용중
|
||||
- temp_4~5: ⭕미사용 (향후 확장용)
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 order_window_brands 테이블: (신규)
|
||||
- temp_1: 브랜드등급(프리미엄/일반/보급) ✅사용중
|
||||
- temp_2: 주력제품군 ✅사용중
|
||||
- temp_3: 가격대 ⭕미사용
|
||||
- temp_4: A/S기간 ⭕미사용
|
||||
- temp_5: 보증기간 ⭕미사용
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 order_config 테이블:
|
||||
- temp_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 order_mail_templates 테이블:
|
||||
- temp_1: 발송대상그룹 ✅사용중
|
||||
- temp_2: 발송조건 ✅사용중
|
||||
- temp_3~5: ⭕미사용 (향후 확장용)
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
🔹 order_sms_templates 테이블:
|
||||
- temp_1: 발송대상그룹 ✅사용중
|
||||
- temp_2: 발송조건 ✅사용중
|
||||
- temp_3~5: ⭕미사용 (향후 확장용)
|
||||
- extra_1~5: ⭕전체 미사용 (향후 확장용)
|
||||
|
||||
=== 📈 전체 사용률 요약 ===
|
||||
✅ 사용중인 컬럼: 19개
|
||||
⭕ 미사용 컬럼: 61개
|
||||
📊 사용률: 23.8%
|
||||
*/
|
||||
@@ -0,0 +1,114 @@
|
||||
-- 1. 견적서 마스터 테이블
|
||||
-- 고객의 견적 요청 기본 정보와 여분 필드를 저장합니다.
|
||||
CREATE TABLE IF NOT EXISTS `estimate` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`wr_id` int DEFAULT NULL COMMENT '원본 게시물 ID',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'requesting' COMMENT '견적 상태 (requesting:신청중, completed:작성완료, bidding:입찰중, selected:채택됨, closed:마감)',
|
||||
`bidding_deadline` DATETIME DEFAULT NULL COMMENT '입찰 마감일시',
|
||||
`consultant_id` VARCHAR(20) DEFAULT NULL COMMENT '배정된 상담가 ID',
|
||||
`consultant_assigned_at` DATETIME DEFAULT NULL COMMENT '상담가 배정일시',
|
||||
`company_name` varchar(100) DEFAULT NULL COMMENT '회사명',
|
||||
`site_name` varchar(100) DEFAULT NULL COMMENT '현장명',
|
||||
`estimate_date` varchar(30) DEFAULT NULL 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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부 (1:사용, 0:미사용)',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `wr_id` (`wr_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='견적서 마스터 정보';
|
||||
|
||||
|
||||
|
||||
-- 2. 견적서 상세 항목 테이블
|
||||
-- 각 창의 상세 사양과 여분 필드를 저장합니다.
|
||||
CREATE TABLE IF NOT EXISTS `estimate_item` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`estimate_id` int DEFAULT NULL COMMENT '견적서 ID (estimate.id)',
|
||||
`no` int DEFAULT NULL COMMENT '항목 번호',
|
||||
`location` varchar(50) DEFAULT NULL COMMENT '위치',
|
||||
`product` varchar(50) DEFAULT NULL COMMENT '품명',
|
||||
`window_main_type` VARCHAR(255) NULL DEFAULT NULL COMMENT '창 종류',
|
||||
`windowRatio` varchar(20) DEFAULT NULL COMMENT '창비율',
|
||||
`windowType` varchar(20) DEFAULT NULL COMMENT '창호형태',
|
||||
`replacePart` varchar(20) DEFAULT NULL COMMENT '교체위치',
|
||||
`color` varchar(20) DEFAULT NULL COMMENT '색상',
|
||||
`spec_width` varchar(20) DEFAULT NULL COMMENT '규격(가로)',
|
||||
`spec_height` varchar(20) DEFAULT NULL COMMENT '규격(세로)',
|
||||
`glass_thickness` varchar(20) DEFAULT NULL COMMENT '유리두께',
|
||||
`glass_color` varchar(20) DEFAULT NULL COMMENT '유리색상',
|
||||
`screen` varchar(10) DEFAULT NULL COMMENT '방충망',
|
||||
`handle` varchar(20) DEFAULT NULL COMMENT '손잡이(시정장치)',
|
||||
`door_dir` varchar(20) DEFAULT NULL COMMENT '문방향',
|
||||
`qty` int DEFAULT NULL COMMENT '수량',
|
||||
`price` int DEFAULT NULL COMMENT '단가',
|
||||
`amount` int DEFAULT NULL COMMENT '금액',
|
||||
`install` varchar(5) DEFAULT NULL COMMENT '시공여부',
|
||||
`brand` varchar(30) DEFAULT NULL 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',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부 (1:사용, 0:미사용)',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`created_by` varchar(20) DEFAULT NULL COMMENT '생성자',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `estimate_id` (`estimate_id`),
|
||||
CONSTRAINT `fk_item_to_estimate` FOREIGN KEY (`estimate_id`) REFERENCES `estimate` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='견적서 상세 항목';
|
||||
|
||||
|
||||
-- 3. 견적 및 항목 변경 이력 테이블
|
||||
-- 모든 데이터의 생성, 수정, 삭제 이력을 기록합니다.
|
||||
CREATE TABLE IF NOT EXISTS `estimate_history` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '이력 고유 ID',
|
||||
`estimate_id` int NOT NULL COMMENT '견적서 ID',
|
||||
`item_id` int DEFAULT NULL COMMENT '견적 항목 ID (항목 변경 시)',
|
||||
`action` varchar(20) NOT NULL COMMENT '작업 종류 (create, update, delete)',
|
||||
`change_details` longtext COMMENT '변경된 데이터 (JSON 형식)',
|
||||
`changed_by` varchar(20) NOT NULL COMMENT '변경자 ID',
|
||||
`changed_at` datetime NOT NULL COMMENT '변경일시',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `estimate_id` (`estimate_id`),
|
||||
KEY `item_id` (`item_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='견적 및 항목 변경 이력';
|
||||
|
||||
-- 4. 대리점 입찰 정보 테이블
|
||||
-- 대리점이 고객의 견적 요청에 대해 제출한 입찰(견적) 정보를 저장합니다.
|
||||
CREATE TABLE IF NOT EXISTS `estimate_bidding` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`estimate_id` int NOT NULL COMMENT '견적서 ID (estimate.id)',
|
||||
`dealer_id` varchar(20) NOT NULL COMMENT '입찰한 대리점 회원 ID',
|
||||
`bid_amount` int NOT NULL COMMENT '대리점 제시 금액',
|
||||
`bid_message` text COMMENT '대리점 메모',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'bidding' COMMENT '입찰 상태 (bidding:입찰중, selected:채택됨, unselected:미채택)',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '수정일시',
|
||||
`updated_by` varchar(20) DEFAULT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `estimate_id` (`estimate_id`),
|
||||
KEY `dealer_id` (`dealer_id`),
|
||||
CONSTRAINT `fk_bidding_to_estimate` FOREIGN KEY (`estimate_id`) REFERENCES `estimate` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='대리점 입찰 정보';
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
/**
|
||||
* SQL 파일을 기반으로 데이터베이스 스키마를 관리(생성/업데이트)하는 범용 클래스
|
||||
*/
|
||||
class SchemaManager
|
||||
{
|
||||
private $sql_file_path;
|
||||
private $results;
|
||||
private $conn; // DB 연결 객체를 저장할 변수
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
* @param string $sql_file_path install.sql 파일의 절대 경로
|
||||
*/
|
||||
public function __construct($sql_file_path)
|
||||
{
|
||||
global $g5; // 그누보드 DB 연결 객체에 접근하기 위해 global 선언
|
||||
$this->conn = $g5['connect_db']; // DB 연결 객체를 저장
|
||||
|
||||
if (!file_exists($sql_file_path)) {
|
||||
throw new Exception($sql_file_path . ' 파일을 찾을 수 없습니다.');
|
||||
}
|
||||
$this->sql_file_path = $sql_file_path;
|
||||
$this->results = [
|
||||
'created' => [],
|
||||
'existing' => [],
|
||||
'updated' => [],
|
||||
'failed' => [],
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스키마 설치/업데이트를 실행합니다.
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$sql_statements = $this->parse_sql_file();
|
||||
|
||||
foreach ($sql_statements as $stmt) {
|
||||
// CREATE TABLE 문인지 확인
|
||||
if (preg_match('/^CREATE\s+TABLE/i', $stmt)) {
|
||||
$schema = $this->parse_create_table_sql($stmt);
|
||||
if ($schema && !empty($schema['name'])) {
|
||||
$this->process_table_schema($stmt, $schema);
|
||||
}
|
||||
} else {
|
||||
// CREATE TABLE 문이 아닌 다른 SQL 문 (e.g. INSERT, UPDATE)
|
||||
mysqli_query($this->conn, $stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 처리 결과를 반환합니다.
|
||||
* @return array
|
||||
*/
|
||||
public function get_results()
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 스키마를 처리합니다. (생성 또는 업데이트)
|
||||
* @param string $create_sql 전체 CREATE TABLE 구문
|
||||
* @param array $schema 파싱된 스키마 정보
|
||||
*/
|
||||
private function process_table_schema($create_sql, $schema)
|
||||
{
|
||||
$table_name = $schema['name'];
|
||||
|
||||
if ($this->table_exists($table_name)) {
|
||||
// 테이블이 존재하면, 컬럼 비교 및 추가/수정
|
||||
$this->results['existing'][] = $table_name;
|
||||
$this->update_table_columns($table_name, $schema['columns']);
|
||||
} else {
|
||||
// 테이블이 존재하지 않으면, 새로 생성
|
||||
if (mysqli_query($this->conn, $create_sql)) {
|
||||
$this->results['created'][] = $table_name;
|
||||
} else {
|
||||
$this->results['failed'][] = $table_name;
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블 생성 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 컬럼 구조를 업데이트합니다.
|
||||
* @param string $table_name
|
||||
* @param array $target_columns .sql 파일에 정의된 컬럼 목록
|
||||
*/
|
||||
private function update_table_columns($table_name, $target_columns)
|
||||
{
|
||||
$current_columns = $this->get_current_columns($table_name);
|
||||
$added_columns_in_table = [];
|
||||
|
||||
foreach ($target_columns as $col_name => $col_definition) {
|
||||
// 현재 테이블에 해당 컬럼이 없으면 추가
|
||||
if (!isset($current_columns[$col_name])) {
|
||||
$alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$col_name}` {$col_definition}";
|
||||
if (mysqli_query($this->conn, $alter_sql)) {
|
||||
$added_columns_in_table[] = $col_name;
|
||||
} else {
|
||||
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블에 '{$col_name}' 컬럼 추가 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
} else {
|
||||
// 💡 [핵심 수정] 컬럼이 이미 존재하면, 코멘트 등을 업데이트하기 위해 MODIFY 실행
|
||||
$alter_sql = "ALTER TABLE `{$table_name}` MODIFY COLUMN `{$col_name}` {$col_definition}";
|
||||
if (!mysqli_query($this->conn, $alter_sql)) {
|
||||
// MODIFY 실패 시 에러 기록
|
||||
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블의 '{$col_name}' 컬럼 수정 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($added_columns_in_table)) {
|
||||
$this->results['updated'][$table_name] = $added_columns_in_table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 파일을 읽고 각 구문으로 분리합니다.
|
||||
* @return array
|
||||
*/
|
||||
private function parse_sql_file()
|
||||
{
|
||||
$sql = file_get_contents($this->sql_file_path);
|
||||
// 주석 제거 (SQL 주석 '--' 와 C-style '/* ... */' 주석)
|
||||
$sql = preg_replace('/--.*/', '', $sql);
|
||||
$sql = preg_replace('!/\*.*?\*/!s', '', $sql);
|
||||
$sql = trim($sql);
|
||||
|
||||
// 세미콜론(;)을 기준으로 쿼리 분리
|
||||
return array_filter(array_map('trim', explode(';', $sql)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 존재 여부를 확인합니다.
|
||||
* @param string $table_name
|
||||
* @return bool
|
||||
*/
|
||||
private function table_exists($table_name)
|
||||
{
|
||||
$res = mysqli_query($this->conn, "SHOW TABLES LIKE '{$table_name}'");
|
||||
return mysqli_num_rows($res) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 DB에 있는 테이블의 컬럼 목록을 가져옵니다.
|
||||
* @param string $table_name
|
||||
* @return array
|
||||
*/
|
||||
private function get_current_columns($table_name)
|
||||
{
|
||||
$res = mysqli_query($this->conn, "SHOW COLUMNS FROM `{$table_name}`");
|
||||
$columns = [];
|
||||
while ($row = mysqli_fetch_array($res)) {
|
||||
$columns[$row['Field']] = true;
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE SQL 구문에서 테이블명과 컬럼 정의를 파싱합니다.
|
||||
* @param string $query CREATE TABLE 구문
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_create_table_sql($query)
|
||||
{
|
||||
$table_name = '';
|
||||
if (preg_match('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?(\w+)`?/i', $query, $matches)) {
|
||||
$table_name = $matches[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 괄호 안의 내용만 추출
|
||||
$start = strpos($query, '(');
|
||||
$end = strrpos($query, ')');
|
||||
if ($start === false || $end === false) {
|
||||
return ['name' => $table_name, 'columns' => []];
|
||||
}
|
||||
$content = substr($query, $start + 1, $end - $start - 1);
|
||||
|
||||
// 줄 단위로 분리
|
||||
$lines = explode("\n", $content);
|
||||
|
||||
$columns = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line, " \t\n\r\0\x0B,"); // 양쪽 공백과 마지막 쉼표 제거
|
||||
|
||||
// 컬럼 정의 라인인지 확인 (첫 단어가 `column_name` 형태)
|
||||
if (preg_match('/^`(\w+)`\s+(.*)/i', $line, $match)) {
|
||||
$col_name = $match[1];
|
||||
$col_definition = $match[2];
|
||||
$columns[$col_name] = $col_definition;
|
||||
}
|
||||
}
|
||||
|
||||
return ['name' => $table_name, 'columns' => $columns];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// ❗ [핵심] NotificationSender 클래스 포함
|
||||
if (file_exists(__DIR__ . '/notification_sender.php')) {
|
||||
require_once(__DIR__ . '/notification_sender.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* 주문 설정값을 가져옵니다.
|
||||
*/
|
||||
function get_order_config($key, $default = '')
|
||||
{
|
||||
$result = sql_fetch("SELECT config_value FROM order_config WHERE config_key = '" . sql_real_escape_string($key) . "'");
|
||||
return $result ? $result['config_value'] : $default;
|
||||
}
|
||||
|
||||
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}' 파일에 내용을 쓸 수 없습니다.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* ❗ [핵심 수정] 1. 견적 상태 변경 시 알림 발송
|
||||
* @param int $wr_id 상태가 변경된 게시물 ID (원본 또는 답변글)
|
||||
* @param string $new_status 새로 변경된 상태
|
||||
*/
|
||||
function notify_by_status($wr_id, $new_status)
|
||||
{
|
||||
global $config;
|
||||
$post = sql_fetch("SELECT * FROM {$GLOBALS['g5']['write_prefix']}order WHERE wr_id = '{$wr_id}'");
|
||||
if (!$post) return false;
|
||||
write_debug_log("[notify_by_status] 원본 견적(estimate) 시작 : {$wr_id}");
|
||||
// 원본글 정보 조회
|
||||
$origin_post = sql_fetch("SELECT * FROM {$GLOBALS['g5']['write_prefix']}order WHERE wr_num = '{$post['wr_num']}' AND wr_reply = ''");
|
||||
if (!$origin_post) return false;
|
||||
$customer = get_member($origin_post['mb_id']);
|
||||
if (!$customer) return false;
|
||||
|
||||
// 템플릿에 사용할 공통 변수 (영문 키로 표준화)
|
||||
$common_vars = [
|
||||
'customer_name' => $customer['mb_name'],
|
||||
'request_title' => $origin_post['wr_subject'],
|
||||
'request_date' => date('Y-m-d', strtotime($origin_post['wr_datetime'])),
|
||||
'write_url' => get_pretty_url('order', $origin_post['wr_id']),
|
||||
'site_name' => $config['cf_title'] ?? '우리집창호', // 사이트 이름 추가
|
||||
];
|
||||
|
||||
if (!class_exists('NotificationSender')) return false;
|
||||
$sender = new NotificationSender();
|
||||
|
||||
// ❗ [추가] 결제 및 진행상태 관련 알림을 위한 공통 변수 처리
|
||||
$payment_statuses = ['계약금입금완료', '중도금입금예정', '중도금입금완료', '시공완료'];
|
||||
$payment_vars = [];
|
||||
$agent_member = null;
|
||||
$bid_dealer_id = null;
|
||||
|
||||
if (in_array($new_status, $payment_statuses)) {
|
||||
// ❗ [핵심 수정] estimate_bidding을 사용하지 않고, 채택된 답변글에서 직접 정보를 가져옵니다.
|
||||
// 1. 원본글의 estimate 정보를 가져옵니다. (시공일, 중도금일 등)
|
||||
$origin_estimate = sql_fetch("SELECT * FROM estimate WHERE wr_id = '{$origin_post['wr_id']}'");
|
||||
if (!$origin_estimate) {
|
||||
write_debug_log("[notify_by_status] 원본 견적(estimate) 정보를 찾을 수 없습니다. wr_id: {$origin_post['wr_id']}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 채택된 답변글(대리점 제안) 정보를 찾습니다.
|
||||
$selected_reply_sql = "
|
||||
SELECT
|
||||
w.mb_id as dealer_id,
|
||||
e.commission_fee as total_amount
|
||||
FROM
|
||||
{$GLOBALS['g5']['write_prefix']}order w
|
||||
JOIN
|
||||
estimate e ON w.wr_id = e.wr_id
|
||||
WHERE
|
||||
w.wr_num = '{$origin_post['wr_num']}'
|
||||
AND w.wr_reply != ''
|
||||
AND e.status = '견적채택'
|
||||
LIMIT 1
|
||||
";
|
||||
$selected_bid_info = sql_fetch($selected_reply_sql);
|
||||
|
||||
if ($selected_bid_info) {
|
||||
$bid_dealer_id = $selected_bid_info['dealer_id'];
|
||||
$agent_member = get_member($bid_dealer_id);
|
||||
$total_amount = (int) $selected_bid_info['total_amount']; // 답변글의 estimate.commission_fee를 총액으로 사용
|
||||
|
||||
$deposit_rate = (float) get_order_config('contract_deposit_rate', 10);
|
||||
$interim_rate = (float) get_order_config('middle_payment_rate', 40);
|
||||
$deposit_amount = (int) ($total_amount * $deposit_rate / 100);
|
||||
$interim_amount = (int) ($total_amount * $interim_rate / 100);
|
||||
|
||||
$payment_vars = array_merge($common_vars, [
|
||||
'deposit_amount' => number_format($deposit_amount) . '원',
|
||||
'interim_amount' => number_format($interim_amount) . '원',
|
||||
'agent_name' => $agent_member['mb_name'] ?? '담당 대리점',
|
||||
'construction_date' => $origin_estimate['temp_6'] ?? '미정',
|
||||
'interim_payment_date' => $origin_estimate['temp_7'] ?? '미정',
|
||||
'account_info' => get_order_config('payment_account_info', '계좌 정보 미설정'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($payment_vars)) { // 필수 정보가 없으면 발송 중단
|
||||
write_debug_log("[notify_by_status] 결제 정보 부족으로 '{$new_status}' 알림 발송을 건너뜁니다. wr_id: {$wr_id}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch ($new_status) {
|
||||
case '작성완료':
|
||||
// 1. 고객에게 알림
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $customer['mb_id'],
|
||||
'email_template_key' => 'customer_request_complete',
|
||||
'sms_template_key' => 'customer_request_complete',
|
||||
'vars' => $common_vars,
|
||||
]);
|
||||
|
||||
// 2. 모든 대리점에게 알림
|
||||
$sender->send([
|
||||
'target_type' => 'bulk',
|
||||
'member_levels' => [9,10],
|
||||
'email_template_key' => 'agent_new_request',
|
||||
'sms_template_key' => 'agent_new_request',
|
||||
'vars' => $common_vars,
|
||||
]);
|
||||
break;
|
||||
|
||||
case '견적채택':
|
||||
$agent_member = get_member($post['mb_id']);
|
||||
write_debug_log("[notify_by_status] agent_member=> ".$agent_member['mb_id'].', name'.$agent_member['mb_name'].', id '.$post['mb_id'].', w_id '.$wr_id);
|
||||
if (!$agent_member) break;
|
||||
|
||||
// 🔥 [핵심 수정] 채택된 답변글(wr_id)의 estimate 레코드에서 최종 금액을 가져옵니다.
|
||||
$agent_estimate = sql_fetch("SELECT id, commission_fee FROM estimate WHERE wr_id = '{$post['wr_id']}'");
|
||||
$final_amount = $agent_estimate ? (int)$agent_estimate['commission_fee'] : 0;
|
||||
|
||||
// [수정] 입찰 정보를 바탕으로 변수를 설정합니다.
|
||||
$bid_vars = array_merge($common_vars, [
|
||||
'agent_name' => $agent_member['mb_name'],
|
||||
'dealer_name' => $agent_member['mb_name'], // 템플릿 호환용
|
||||
'deposit_amount' => number_format($final_amount) . '원', // 최종 금액을 입금 안내
|
||||
'bid_amount' => number_format($final_amount), // 템플릿 호환용
|
||||
'account_info' => get_order_config('payment_account_info', '계좌 정보 미설정'),
|
||||
'estimate_id' => $agent_estimate ? $agent_estimate['id'] : 'N/A',
|
||||
'selected_date' => date('Y-m-d H:i'),
|
||||
'estimate_url' => get_pretty_url('order', $origin_post['wr_id']),
|
||||
]);
|
||||
// 1. 채택된 대리점에게 알림 (축하 메시지)
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $agent_member['mb_id'],
|
||||
'email_template_key' => 'bid_selected_email',
|
||||
'sms_template_key' => 'bid_selected_sms',
|
||||
'vars' => $bid_vars,
|
||||
]);
|
||||
|
||||
// 2. 고객에게 알림 (선택 완료 및 입금 안내)
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $customer['mb_id'],
|
||||
'email_template_key' => 'customer_quote_selected',
|
||||
'sms_template_key' => 'customer_quote_selected',
|
||||
'vars' => $bid_vars,
|
||||
]);
|
||||
break;
|
||||
|
||||
case '계약금입금완료':
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $customer['mb_id'],
|
||||
'email_template_key' => 'payment_deposit_complete_customer',
|
||||
'sms_template_key' => 'payment_deposit_complete_customer',
|
||||
'vars' => $payment_vars,
|
||||
]);
|
||||
break;
|
||||
|
||||
case '중도금입금예정':
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $customer['mb_id'],
|
||||
'email_template_key' => 'payment_interim_due_customer',
|
||||
'sms_template_key' => 'payment_interim_due_customer',
|
||||
'vars' => $payment_vars,
|
||||
]);
|
||||
break;
|
||||
|
||||
case '중도금입금완료':
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $customer['mb_id'],
|
||||
'email_template_key' => 'payment_interim_complete_customer',
|
||||
'sms_template_key' => 'payment_interim_complete_customer',
|
||||
'vars' => $payment_vars,
|
||||
]);
|
||||
break;
|
||||
|
||||
case '시공완료':
|
||||
// 고객에게 알림
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $customer['mb_id'],
|
||||
'email_template_key' => 'construction_complete_customer',
|
||||
'sms_template_key' => 'construction_complete_customer',
|
||||
'vars' => $payment_vars,
|
||||
]);
|
||||
|
||||
// 대리점에게 알림
|
||||
if ($bid_dealer_id) {
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $bid_dealer_id,
|
||||
'email_template_key' => 'construction_complete_agent',
|
||||
'sms_template_key' => 'construction_complete_agent',
|
||||
'vars' => $payment_vars,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [신규] 2. 입금 관련 알림 발송 (관리자, 고객)
|
||||
* @param int $wr_id 원본 게시물 ID
|
||||
* @param string $payment_status 입금 상태 (예: '계약금입금완료')
|
||||
* @param array $payment_info 추가 결제 정보 (금액 등)
|
||||
*/
|
||||
function notify_for_payment($wr_id, $payment_status, $payment_info = [])
|
||||
{
|
||||
$origin_post = sql_fetch("SELECT * FROM {$GLOBALS['g5']['write_prefix']}order WHERE wr_id = '{$wr_id}'");
|
||||
if (!$origin_post) return false;
|
||||
$customer = get_member($origin_post['mb_id']);
|
||||
if (!$customer) return false;
|
||||
|
||||
$common_vars = array_merge([
|
||||
'customer_name' => $customer['mb_name'],
|
||||
'estimate_subject' => $origin_post['wr_subject'],
|
||||
], $payment_info);
|
||||
|
||||
if (!class_exists('NotificationSender')) return false;
|
||||
$sender = new NotificationSender();
|
||||
|
||||
// 1. 고객에게 알림
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $customer['mb_id'],
|
||||
'email_template_key' => 'payment_status_update_customer', // 예시 템플릿 키
|
||||
'sms_template_key' => 'payment_status_update_customer',
|
||||
'vars' => $common_vars,
|
||||
]);
|
||||
|
||||
// 2. 관리자에게 알림
|
||||
$sender->send([
|
||||
'target_type' => 'bulk',
|
||||
'member_levels' => [10], // 관리자 레벨
|
||||
'email_template_key' => 'payment_status_update_admin', // 예시 템플릿 키
|
||||
'sms_template_key' => 'payment_status_update_admin',
|
||||
'vars' => $common_vars,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [신규] 3. 전문가 방문 예약 관련 알림 발송 (고객, 전문가, 관리자)
|
||||
* @param int $reservation_id 예약 ID
|
||||
* @param string $visit_status 방문 상태 (예: '신규예약', '예약확정', '예약취소')
|
||||
*/
|
||||
function notify_for_expert_visit($reservation_id, $visit_status)
|
||||
{
|
||||
// 예약 정보 조회
|
||||
$reservation = sql_fetch("SELECT * FROM expert_visit_reservations WHERE id = '{$reservation_id}'");
|
||||
if (!$reservation) return false;
|
||||
|
||||
// 💡 [수정] 고객 정보 조회 (회원/비회원 구분)
|
||||
$customer = get_member($reservation['customer_id']);
|
||||
$is_guest = empty($customer['mb_id']);
|
||||
|
||||
// 비회원일 경우 예약 정보에서 직접 가져옴
|
||||
$customer_name = $is_guest ? $reservation['customer_name'] : $customer['mb_name'];
|
||||
$customer_hp = $is_guest ? $reservation['customer_phone'] : $customer['mb_hp'];
|
||||
$customer_email = $is_guest ? $reservation['customer_email'] : $customer['mb_email'];
|
||||
|
||||
$expert = get_member($reservation['expert_id']);
|
||||
|
||||
$common_vars = [
|
||||
'customer_name' => $customer_name,
|
||||
'visit_date' => $reservation['visit_date'],
|
||||
'visit_time' => $reservation['visit_time'],
|
||||
'expert_name' => $expert['mb_name'] ?? '미지정',
|
||||
];
|
||||
|
||||
if (!class_exists('NotificationSender')) return false;
|
||||
$sender = new NotificationSender();
|
||||
|
||||
switch ($visit_status) {
|
||||
case '신규예약':
|
||||
// 1. 고객에게 신청 완료 알림
|
||||
$send_params = [
|
||||
'email_template_key' => 'expert_visit_request_new',
|
||||
'sms_template_key' => 'expert_visit_request_new',
|
||||
'vars' => array_merge($common_vars, [
|
||||
'reservation_id' => $reservation_id,
|
||||
'expert_visit_fee' => number_format($reservation['payment_amount']) . '원',
|
||||
'account_info' => get_order_config('expert_visit_account_info', '계좌 정보 미설정'),
|
||||
]),
|
||||
];
|
||||
|
||||
if ($is_guest) {
|
||||
// 비회원: 직접 수신자 정보 지정
|
||||
$send_params['target_type'] = 'direct';
|
||||
$send_params['receivers'] = [
|
||||
[
|
||||
'mb_name' => $customer_name,
|
||||
'mb_hp' => $customer_hp,
|
||||
'mb_email' => $customer_email,
|
||||
'mb_sms' => 1, // SMS 수신 동의 가정
|
||||
'mb_mailling' => 1 // 메일 수신 동의 가정
|
||||
]
|
||||
];
|
||||
} else {
|
||||
// 회원: member_id 사용
|
||||
$send_params['target_type'] = 'single';
|
||||
$send_params['member_id'] = $customer['mb_id'];
|
||||
}
|
||||
$sender->send($send_params);
|
||||
|
||||
// 2. 관리자에게 알림
|
||||
$sender->send([
|
||||
'target_type' => 'bulk',
|
||||
'member_levels' => [10],
|
||||
'email_template_key' => 'admin_new_visit_request',
|
||||
'sms_template_key' => 'admin_new_visit_request',
|
||||
'vars' => array_merge($common_vars, [
|
||||
'customer_phone' => $customer_hp,
|
||||
'request_memo' => $reservation['request_memo'],
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
|
||||
case '예약확정':
|
||||
// 1. 고객에게 예약 확정 알림
|
||||
$send_params = [
|
||||
'email_template_key' => 'expert_visit_confirmed',
|
||||
'sms_template_key' => 'expert_visit_confirmed',
|
||||
'vars' => $common_vars,
|
||||
];
|
||||
|
||||
if ($is_guest) {
|
||||
$send_params['target_type'] = 'direct';
|
||||
$send_params['receivers'] = [
|
||||
[
|
||||
'mb_name' => $customer_name,
|
||||
'mb_hp' => $customer_hp,
|
||||
'mb_email' => $customer_email,
|
||||
'mb_sms' => 1,
|
||||
'mb_mailling' => 1
|
||||
]
|
||||
];
|
||||
} else {
|
||||
$send_params['target_type'] = 'single';
|
||||
$send_params['member_id'] = $customer['mb_id'];
|
||||
}
|
||||
$sender->send($send_params);
|
||||
|
||||
// 2. 배정된 전문가에게 알림
|
||||
if ($expert) {
|
||||
$sender->send([
|
||||
'target_type' => 'single',
|
||||
'member_id' => $expert['mb_id'],
|
||||
'email_template_key' => 'expert_new_assignment',
|
||||
'sms_template_key' => 'expert_new_assignment',
|
||||
'vars' => $common_vars,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case '예약취소':
|
||||
// 1. 고객에게 예약 취소 알림
|
||||
$send_params = [
|
||||
'email_template_key' => 'expert_visit_cancelled',
|
||||
'sms_template_key' => 'expert_visit_cancelled',
|
||||
'vars' => $common_vars,
|
||||
];
|
||||
|
||||
if ($is_guest) {
|
||||
$send_params['target_type'] = 'direct';
|
||||
$send_params['receivers'] = [
|
||||
[
|
||||
'mb_name' => $customer_name,
|
||||
'mb_hp' => $customer_hp,
|
||||
'mb_email' => $customer_email,
|
||||
'mb_sms' => 1,
|
||||
'mb_mailling' => 1
|
||||
]
|
||||
];
|
||||
} else {
|
||||
$send_params['target_type'] = 'single';
|
||||
$send_params['member_id'] = $customer['mb_id'];
|
||||
}
|
||||
$sender->send($send_params);
|
||||
|
||||
// 2. 관리자에게 알림
|
||||
$sender->send([
|
||||
'target_type' => 'bulk',
|
||||
'member_levels' => [10],
|
||||
'email_template_key' => 'admin_visit_cancelled',
|
||||
'sms_template_key' => 'admin_visit_cancelled',
|
||||
'vars' => $common_vars,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,380 @@
|
||||
<?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')) {
|
||||
// 💡 [수정] 라이브러리 원본 파일(icode.lms.lib.php 등)의 Deprecated 경고를 숨기기 위해 에러 리포팅 임시 조정
|
||||
// (PHP 8.0+에서 선택적 파라미터 선언 순서 문제로 인한 경고 회피)
|
||||
$old_reporting_level = error_reporting();
|
||||
error_reporting($old_reporting_level & ~E_DEPRECATED);
|
||||
|
||||
include_once(G5_PLUGIN_PATH . '/sms5/sms5.lib.php');
|
||||
|
||||
error_reporting($old_reporting_level);
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 메일/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 ($params['target_type'] === 'direct' && empty($params['receivers'])) {
|
||||
return ['success' => false, 'message' => '직접 발송 시 수신자 정보(receivers)가 필요합니다.'];
|
||||
}
|
||||
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 = ''";
|
||||
$this->write_debug_log("[SMS 발송 시작] sql '{$sql}'");
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$members[] = $row;
|
||||
}
|
||||
} elseif ($params['target_type'] === 'bulk') {
|
||||
$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;
|
||||
}
|
||||
} elseif ($params['target_type'] === 'direct') {
|
||||
// 직접 입력된 수신자 정보 사용
|
||||
$members = $params['receivers'];
|
||||
}
|
||||
|
||||
return $members;
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [핵심 수정] SMS 발송 (템플릿 및 변수 처리, 이력 기록 포함)
|
||||
*/
|
||||
private function sendSMS($members, $template_key, $common_vars)
|
||||
{
|
||||
global $config; // config 전역 변수 사용
|
||||
|
||||
$success = 0;
|
||||
$fail = 0;
|
||||
$notification_mode = get_order_config('notification_mode', 'log');
|
||||
$is_test_mode = ($notification_mode !== 'send');
|
||||
|
||||
$sizeof_members = count($members);
|
||||
$template = sql_fetch("SELECT * FROM `order_sms_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "' ");
|
||||
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';
|
||||
$hs_code = '';
|
||||
|
||||
if ($SMS) {
|
||||
// ❗ [수정] SMS/LMS 타입에 따라 함수 호출 분기 및 파라미터 순서 명시
|
||||
if($config['cf_sms_type'] == 'LMS') {
|
||||
// LMS: Add($strDest, $strCallBack, $strCaller, $strSubject, $strURL, $strData, $strDate, $nCount)
|
||||
// $strDest는 배열로 전달
|
||||
$SMS->Add(array($member['mb_hp']), $send_phone, '', '', '', $personal_message, '', 1);
|
||||
} else {
|
||||
// SMS: Add2($strDest, $strCallBack, $strCaller, $strURL, $strMessage, $strDate, $nCount)
|
||||
// $strDest는 배열(구조체)로 전달
|
||||
$dest = array(array('bk_hp' => $member['mb_hp'], 'bk_name' => $member['mb_name']));
|
||||
$SMS->Add2($dest, $send_phone, '', '', $personal_message, '', 1);
|
||||
}
|
||||
|
||||
$SMS->Send();
|
||||
$result_arr = $SMS->Result;
|
||||
$result_code = 'Fail';
|
||||
$result_msg = '서버로부터 응답이 없습니다.';
|
||||
|
||||
if(!empty($result_arr)){
|
||||
// 💡 [추가] 상세 에러 코드 처리 (sms_write_send.php 참고)
|
||||
foreach ($result_arr as $result) {
|
||||
list($phone, $code) = explode(":", $result);
|
||||
if (substr($code, 0, 5) == "Error") {
|
||||
$hs_code = substr($code, 6, 2);
|
||||
switch ($hs_code) {
|
||||
case '02': $result_msg = "형식이 잘못되어 전송이 실패하였습니다."; break;
|
||||
case '23': $result_msg = "데이터를 다시 확인해 주시기바랍니다."; break;
|
||||
case '97': $result_msg = "잔여코인이 부족합니다."; break;
|
||||
case '98': $result_msg = "사용기간이 만료되었습니다."; break;
|
||||
case '99': $result_msg = "인증 받지 못하였습니다. 계정을 다시 확인해 주세요."; break;
|
||||
default: $result_msg = "알 수 없는 오류로 전송이 실패하였습니다."; break;
|
||||
}
|
||||
$result_code = 'Fail';
|
||||
} else {
|
||||
$hs_code = $code;
|
||||
$result_msg = "전송했습니다.";
|
||||
$result_code = 'Success';
|
||||
}
|
||||
}
|
||||
}
|
||||
$hs_status = ($result_code == 'Success') ? '1' : '0';
|
||||
$SMS->Init();
|
||||
}
|
||||
|
||||
$mb_id = isset($member['mb_id']) ? $member['mb_id'] : ''; // 비회원일 경우 mb_id 없음
|
||||
sql_query("INSERT INTO {$this->g5['sms5_history_table']} (wr_no, mb_id, hs_name, hs_hp, hs_datetime, hs_status, hs_code, hs_message) VALUES ('{$wr_no}', '{$mb_id}', '{$member['mb_name']}', '{$member['mb_hp']}', '" . G5_TIME_YMDHIS . "', '{$hs_status}', '{$hs_code}', '{$result_msg}')");
|
||||
|
||||
if ($hs_status == '1') $success++;
|
||||
else $fail++;
|
||||
} else {
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 [추가] 발송 완료 후 마스터 테이블 업데이트
|
||||
sql_query("UPDATE {$this->g5['sms5_write_table']} SET wr_success = '{$success}', wr_failure = '{$fail}' WHERE wr_no = '{$wr_no}'");
|
||||
}
|
||||
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);
|
||||
$template = sql_fetch("SELECT * FROM `order_mail_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
|
||||
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']]);
|
||||
$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);
|
||||
}
|
||||
if($mailSender->sendSimple($member['mb_email'], $subject,$content)){
|
||||
$success++;
|
||||
} else {
|
||||
$fail++;
|
||||
}
|
||||
|
||||
} else {
|
||||
$fail++;
|
||||
$empty = !empty($member['mb_email']);
|
||||
$this->write_debug_log("[EMAIL 발송 오류] 사용자 '{$member['mb_sms']}' '{$member['mb_hp']}' '$empty'");
|
||||
}
|
||||
}
|
||||
}
|
||||
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,95 @@
|
||||
<?php
|
||||
$sub_menu = "800500";
|
||||
include_once("./_common.php");
|
||||
|
||||
auth_check_menu($auth, $sub_menu, "r");
|
||||
|
||||
$id = (int) $_GET['id'];
|
||||
$template = sql_fetch("SELECT * FROM order_mail_templates WHERE id = {$id}");
|
||||
|
||||
if (!$template) {
|
||||
alert('템플릿을 찾을 수 없습니다.', './mail_templates.php');
|
||||
}
|
||||
|
||||
$g5['title'] = "메일 템플릿 수정";
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
$variables = json_decode($template['variables'], true);
|
||||
?>
|
||||
|
||||
<div class="local_ov01 local_ov">
|
||||
메일 템플릿 수정 - <?php echo htmlspecialchars($template['template_name']); ?>
|
||||
</div>
|
||||
|
||||
<form name="templateForm" method="post" action="mail_template_update.php">
|
||||
<input type="hidden" name="id" value="<?php echo $template['id']; ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>메일 템플릿 수정</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">템플릿명</th>
|
||||
<td>
|
||||
<input type="text" name="template_name"
|
||||
value="<?php echo htmlspecialchars($template['template_name']); ?>" class="frm_input"
|
||||
style="width:100%;" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">메일 제목</th>
|
||||
<td>
|
||||
<input type="text" name="subject" value="<?php echo htmlspecialchars($template['subject']); ?>"
|
||||
class="frm_input" style="width:100%;" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">메일 내용</th>
|
||||
<td>
|
||||
<textarea name="content" class="frm_input" style="width:100%; height:300px;"
|
||||
required><?php echo htmlspecialchars($template['content']); ?></textarea>
|
||||
<div class="frm_info">HTML 태그 사용 가능합니다.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">사용 가능한 변수</th>
|
||||
<td>
|
||||
<?php if (is_array($variables)) { ?>
|
||||
<?php foreach ($variables as $var) { ?>
|
||||
<span class="variable-tag">{<?php echo $var; ?>}</span>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<div class="frm_info">위 변수들을 메일 제목과 내용에 사용할 수 있습니다.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="저장" class="btn_submit">
|
||||
<a href="mail_templates.php" class="btn btn_02">목록</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.variable-tag {
|
||||
display: inline-block;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 2px 8px;
|
||||
margin: 2px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
$sub_menu = "800500";
|
||||
include_once("./_common.php");
|
||||
|
||||
auth_check_menu($auth, $sub_menu, "w");
|
||||
|
||||
if ($_POST) {
|
||||
$id = (int) $_POST['id'];
|
||||
$template_name = sql_real_escape_string($_POST['template_name']);
|
||||
$subject = sql_real_escape_string($_POST['subject']);
|
||||
$content = sql_real_escape_string($_POST['content']);
|
||||
|
||||
$sql = "UPDATE order_mail_templates SET
|
||||
template_name = '{$template_name}',
|
||||
subject = '{$subject}',
|
||||
content = '{$content}',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$id}";
|
||||
|
||||
sql_query($sql);
|
||||
|
||||
alert('메일 템플릿이 수정되었습니다.', './mail_templates.php');
|
||||
} else {
|
||||
alert('잘못된 접근입니다.', './mail_templates.php');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
$sub_menu = "800500";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '이메일 템플릿 관리';
|
||||
|
||||
// 액션 처리
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
$id = (int) ($_REQUEST['id'] ?? 0);
|
||||
|
||||
// POST 요청 처리 (생성, 수정, 삭제)
|
||||
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
try {
|
||||
if ($action === 'save') {
|
||||
$template_key = clean_xss_tags($_POST['template_key'] ?? '');
|
||||
$template_name = clean_xss_tags($_POST['template_name'] ?? '');
|
||||
$subject = clean_xss_tags($_POST['subject'] ?? '');
|
||||
$content = $_POST['content'] ?? ''; // HTML 내용이므로 clean_xss_tags 사용하지 않음
|
||||
$variables = clean_xss_tags($_POST['variables'] ?? '');
|
||||
|
||||
if (!$template_key || !$template_name || !$subject || !$content) {
|
||||
throw new Exception('필수 항목을 모두 입력해주세요.');
|
||||
}
|
||||
|
||||
// 템플릿 키 중복 확인 (수정 시 제외)
|
||||
if ($id === 0) {
|
||||
$existing = sql_fetch("SELECT id FROM order_mail_templates WHERE template_key = '{$template_key}'");
|
||||
if ($existing) {
|
||||
throw new Exception('이미 존재하는 템플릿 키입니다.');
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'template_key' => $template_key,
|
||||
'template_name' => $template_name,
|
||||
'subject' => $subject,
|
||||
'content' => $content,
|
||||
'variables' => $variables,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
// 수정
|
||||
$set_clauses = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$set_clauses[] = "`{$key}` = '" . sql_real_escape_string($value) . "'";
|
||||
}
|
||||
$sql = "UPDATE order_mail_templates SET " . implode(', ', $set_clauses) . " WHERE id = '{$id}'";
|
||||
sql_query($sql);
|
||||
$message = '이메일 템플릿이 수정되었습니다.';
|
||||
} else {
|
||||
// 생성
|
||||
$data['created_at'] = date('Y-m-d H:i:s');
|
||||
$fields = array_keys($data);
|
||||
$values = array_values($data);
|
||||
$fields_str = '`' . implode('`, `', $fields) . '`';
|
||||
$values_str = "'" . implode("', '", array_map('sql_real_escape_string', $values)) . "'";
|
||||
$sql = "INSERT INTO order_mail_templates ({$fields_str}) VALUES ({$values_str})";
|
||||
sql_query($sql);
|
||||
$message = '이메일 템플릿이 생성되었습니다.';
|
||||
}
|
||||
|
||||
alert($message, './mail_templates.php');
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
if ($id > 0) {
|
||||
sql_query("DELETE FROM order_mail_templates WHERE id = '{$id}'");
|
||||
alert('이메일 템플릿이 삭제되었습니다.', './mail_templates.php');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert('오류: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
goto_url('./mail_templates.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// GET 요청 처리 (목록, 편집 폼)
|
||||
$mode = isset($_GET['mode']) ? clean_xss_tags($_GET['mode']) : 'list';
|
||||
$template = null;
|
||||
|
||||
if ($mode === 'edit' && $id > 0) {
|
||||
$template = sql_fetch("SELECT * FROM order_mail_templates WHERE id = '{$id}'");
|
||||
if (!$template) {
|
||||
alert('존재하지 않는 템플릿입니다.', './mail_templates.php');
|
||||
}
|
||||
}
|
||||
|
||||
// 템플릿 목록 조회
|
||||
$templates = [];
|
||||
$sql = "SELECT * FROM order_mail_templates ORDER BY template_key ASC";
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$templates[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
이메일 알림에 사용되는 템플릿을 관리합니다.<br>
|
||||
변수를 사용하여 동적인 내용을 삽입할 수 있습니다. 예: {customer_name}, {estimate_id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($mode === 'list'): ?>
|
||||
<!-- 템플릿 목록 -->
|
||||
<div class="local_ov01 local_ov">
|
||||
<span class="btn_ov01">
|
||||
<span class="ov_txt">전체 템플릿 </span>
|
||||
<span class="ov_num"><?php echo count($templates); ?>개</span>
|
||||
</span>
|
||||
<a href="?mode=edit" class="btn btn_02">새 템플릿 추가</a>
|
||||
</div>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">템플릿 키</th>
|
||||
<th scope="col">템플릿명</th>
|
||||
<th scope="col">제목</th>
|
||||
<th scope="col">변수</th>
|
||||
<th scope="col">수정일</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($templates)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="empty_table">등록된 템플릿이 없습니다.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($templates as $tpl): ?>
|
||||
<tr>
|
||||
<td class="td_left">
|
||||
<code><?php echo get_text($tpl['template_key']); ?></code>
|
||||
</td>
|
||||
<td class="td_left">
|
||||
<strong><?php echo get_text($tpl['template_name']); ?></strong>
|
||||
</td>
|
||||
<td class="td_left">
|
||||
<?php echo get_text($tpl['subject']); ?>
|
||||
</td>
|
||||
<td class="td_left">
|
||||
<?php
|
||||
$variables = json_decode($tpl['variables'], true);
|
||||
if (is_array($variables)) {
|
||||
echo '<small>' . implode(', ', array_map(function ($var) {
|
||||
return '{' . $var . '}'; }, $variables)) . '</small>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="td_datetime">
|
||||
<?php echo $tpl['updated_at'] ? date('Y-m-d H:i', strtotime($tpl['updated_at'])) : '-'; ?>
|
||||
</td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<a href="?mode=edit&id=<?php echo $tpl['id']; ?>" class="btn btn_03">수정</a>
|
||||
<a href="javascript:void(0);" onclick="previewTemplate(<?php echo $tpl['id']; ?>)"
|
||||
class="btn btn_04">미리보기</a>
|
||||
<a href="javascript:void(0);" onclick="deleteTemplate(<?php echo $tpl['id']; ?>)"
|
||||
class="btn btn_02">삭제</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- 템플릿 편집 폼 -->
|
||||
<div class="local_desc02 local_desc">
|
||||
<p>
|
||||
<a href="./mail_templates.php" class="btn btn_01">목록으로 돌아가기</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form name="ftemplate" method="post" onsubmit="return validateForm()">
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="id" value="<?php echo $template['id'] ?? 0; ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $template ? '템플릿 수정' : '새 템플릿 추가'; ?></caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="template_key">템플릿 키<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<input type="text" name="template_key" id="template_key"
|
||||
value="<?php echo get_text($template['template_key'] ?? ''); ?>" class="frm_input"
|
||||
maxlength="100" required <?php echo $template ? 'readonly' : ''; ?>>
|
||||
<span class="frm_info">영문, 숫자, 언더스코어만 사용 가능 (예: customer_request_complete)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="template_name">템플릿명<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<input type="text" name="template_name" id="template_name"
|
||||
value="<?php echo get_text($template['template_name'] ?? ''); ?>" class="frm_input"
|
||||
maxlength="255" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="subject">이메일 제목<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<input type="text" name="subject" id="subject"
|
||||
value="<?php echo get_text($template['subject'] ?? ''); ?>" class="frm_input"
|
||||
maxlength="255" required>
|
||||
<span class="frm_info">변수 사용 가능: {customer_name}, {estimate_id} 등</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="content">이메일 내용<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<textarea name="content" id="content" rows="15" class="frm_input"
|
||||
required><?php echo get_text($template['content'] ?? ''); ?></textarea>
|
||||
<span class="frm_info">HTML 태그 사용 가능. 변수는 {변수명} 형태로 입력</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="variables">사용 가능한 변수</label></th>
|
||||
<td>
|
||||
<input type="text" name="variables" id="variables"
|
||||
value="<?php echo get_text($template['variables'] ?? ''); ?>" class="frm_input"
|
||||
placeholder='["customer_name", "estimate_id", "total_amount"]'>
|
||||
<span class="frm_info">JSON 배열 형태로 입력 (예: ["customer_name", "estimate_id"])</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="<?php echo $template ? '수정' : '등록'; ?>" class="btn_submit">
|
||||
<a href="./mail_templates.php" class="btn_cancel">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 미리보기 모달 -->
|
||||
<div id="previewModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>템플릿 미리보기</h3>
|
||||
<span class="close" onclick="closePreview()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="previewContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
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: #fefefe;
|
||||
margin: 5% auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 15px 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 폼 유효성 검사
|
||||
function validateForm() {
|
||||
const templateKey = document.getElementById('template_key').value.trim();
|
||||
const templateName = document.getElementById('template_name').value.trim();
|
||||
const subject = document.getElementById('subject').value.trim();
|
||||
const content = document.getElementById('content').value.trim();
|
||||
|
||||
if (!templateKey || !templateName || !subject || !content) {
|
||||
alert('필수 항목을 모두 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 템플릿 키 형식 검사
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(templateKey)) {
|
||||
alert('템플릿 키는 영문, 숫자, 언더스코어만 사용할 수 있습니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 변수 JSON 형식 검사
|
||||
const variables = document.getElementById('variables').value.trim();
|
||||
if (variables) {
|
||||
try {
|
||||
JSON.parse(variables);
|
||||
} catch (e) {
|
||||
alert('변수는 올바른 JSON 배열 형태로 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 템플릿 삭제
|
||||
function deleteTemplate(id) {
|
||||
if (!confirm('정말로 이 템플릿을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.innerHTML = `
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="${id}">
|
||||
`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// 템플릿 미리보기
|
||||
function previewTemplate(id) {
|
||||
fetch('mail_templates_ajax.php?action=preview&id=' + id)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('previewContent').innerHTML = `
|
||||
<h4>제목: ${data.data.subject}</h4>
|
||||
<div style="border: 1px solid #ddd; padding: 15px; background: white;">
|
||||
${data.data.content}
|
||||
</div>
|
||||
<p><small>사용 가능한 변수: ${data.data.variables || '없음'}</small></p>
|
||||
`;
|
||||
document.getElementById('previewModal').style.display = 'block';
|
||||
} else {
|
||||
alert('미리보기를 불러올 수 없습니다: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('미리보기 중 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
|
||||
// 미리보기 닫기
|
||||
function closePreview() {
|
||||
document.getElementById('previewModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById('previewModal');
|
||||
if (event.target === modal) {
|
||||
closePreview();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
|
||||
// AJAX 요청만 처리
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'GET 요청만 허용됩니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = isset($_GET['action']) ? clean_xss_tags($_GET['action']) : '';
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($action === 'preview' && $id > 0) {
|
||||
$template = sql_fetch("SELECT * FROM order_mail_templates WHERE id = '{$id}'");
|
||||
|
||||
if ($template) {
|
||||
// 변수들을 예시 값으로 치환
|
||||
$variables = json_decode($template['variables'], true);
|
||||
$content = $template['content'];
|
||||
$subject = $template['subject'];
|
||||
|
||||
if (is_array($variables)) {
|
||||
foreach ($variables as $var) {
|
||||
$placeholder = '{' . $var . '}';
|
||||
$example_value = '[' . $var . ']'; // 예: [customer_name]
|
||||
$content = str_replace($placeholder, $example_value, $content);
|
||||
$subject = str_replace($placeholder, $example_value, $subject);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'subject' => $subject,
|
||||
'content' => $content,
|
||||
'variables' => $template['variables']
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => '템플릿을 찾을 수 없습니다.']);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => '잘못된 요청입니다.']);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
$sub_menu = "800700"; // 새로운 메뉴 코드
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '알림 로그 관리';
|
||||
|
||||
$log_file = G5_PATH . '/log/notification_debug.log';
|
||||
$action = isset($_POST['action']) ? clean_xss_tags($_POST['action']) : '';
|
||||
|
||||
// 로그 파일 비우기 처리
|
||||
if ($action === 'clear_log' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
if (file_exists($log_file)) {
|
||||
// 파일을 0바이트로 만듭니다.
|
||||
file_put_contents($log_file, '');
|
||||
alert('알림 로그 파일의 내용을 모두 비웠습니다.', './notification_log.php');
|
||||
} else {
|
||||
alert('로그 파일이 존재하지 않습니다.', './notification_log.php');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 로그 파일 내용 읽기
|
||||
$log_content = '';
|
||||
if (file_exists($log_file) && filesize($log_file) > 0) {
|
||||
// 파일을 라인별로 배열로 읽어옵니다.
|
||||
$lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
// 배열을 뒤집어 최신 로그가 위로 오게 합니다.
|
||||
$reversed_lines = array_reverse($lines);
|
||||
// 다시 문자열로 합칩니다.
|
||||
$log_content = implode("\n", $reversed_lines);
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
시스템에서 발송된 메일 및 SMS의 발송 기록(로그)을 확인합니다.<br>
|
||||
로그 파일은 <code>/log/notification_debug.log</code> 에 저장되며, 최신 기록이 가장 위에 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="local_ov01 local_ov">
|
||||
<form name="fclearlog" method="post" action="./notification_log.php" onsubmit="return confirm('정말로 로그 파일의 모든 내용을 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.');">
|
||||
<input type="hidden" name="action" value="clear_log">
|
||||
<input type="submit" value="로그 비우기" class="btn btn_02">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<section id="sod_fin">
|
||||
<h2 class="h2_frm">로그 내용</h2>
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<textarea id="log_view" readonly style="width: 100%; height: 600px; padding: 10px; border: 1px solid #ccc; background: #f9f9f9; font-family: monospace; font-size: 12px; line-height: 1.5; resize: vertical;"><?php echo htmlspecialchars($log_content); ?></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (empty($log_content)): ?>
|
||||
<div class="local_desc02 local_desc">
|
||||
<p>
|
||||
로그 파일이 없거나 내용이 비어있습니다.
|
||||
p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const textarea = document.getElementById('log_view');
|
||||
if (textarea) {
|
||||
// 페이지 로드 시 스크롤을 맨 위로 이동
|
||||
textarea.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
$sub_menu = "800750";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '알림 발송 테스트';
|
||||
|
||||
// NotificationHelper 로드
|
||||
require_once G5_PATH . '/adm/order_manage/classes/NotificationHelper.class.php';
|
||||
|
||||
// 액션 처리
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
|
||||
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
try {
|
||||
if ($action === 'test_notification') {
|
||||
$template_type = clean_xss_tags($_POST['template_type'] ?? '');
|
||||
$template_key = clean_xss_tags($_POST['template_key'] ?? '');
|
||||
$notification_type = clean_xss_tags($_POST['notification_type'] ?? '');
|
||||
$test_recipient = clean_xss_tags($_POST['test_recipient'] ?? '');
|
||||
|
||||
if (!$template_key || !$notification_type || !$test_recipient) {
|
||||
throw new Exception('필수 항목을 모두 입력해주세요.');
|
||||
}
|
||||
|
||||
// 수신자 유효성 검증
|
||||
if ($notification_type === 'email' && !filter_var($test_recipient, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Exception('유효하지 않은 이메일 주소입니다.');
|
||||
}
|
||||
|
||||
if ($notification_type === 'sms') {
|
||||
$test_recipient = preg_replace('/[^0-9]/', '', $test_recipient);
|
||||
if (!preg_match('/^01[0-9]{8,9}$/', $test_recipient)) {
|
||||
throw new Exception('유효하지 않은 전화번호입니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 테스트 데이터 준비
|
||||
$test_data = [
|
||||
'customer_name' => '테스트 고객',
|
||||
'customer_email' => $notification_type === 'email' ? $test_recipient : 'test@example.com',
|
||||
'customer_phone' => $notification_type === 'sms' ? $test_recipient : '01012345678',
|
||||
'estimate_id' => 'TEST-001',
|
||||
'estimate_subject' => '테스트 견적 요청',
|
||||
'total_amount' => '1,500,000',
|
||||
'visit_fee' => '50,000',
|
||||
'account_info' => '국민은행 123-456-789012 (주)창호전문',
|
||||
'dealer_name' => '테스트 대리점',
|
||||
'construction_date' => '2024-02-15',
|
||||
'visit_datetime' => '2024-02-10 14:00',
|
||||
'interim_payment_date' => '2024-02-13',
|
||||
'days_remaining' => '2',
|
||||
'bid_amount' => '1,200,000',
|
||||
'selected_date' => date('Y-m-d H:i')
|
||||
];
|
||||
|
||||
$notification_helper = new NotificationHelper();
|
||||
|
||||
// 알림 발송
|
||||
if ($template_type === 'customer') {
|
||||
$result = $notification_helper->sendCustomerNotification($template_key, $test_data);
|
||||
} elseif ($template_type === 'dealer') {
|
||||
$result = $notification_helper->sendDealerNotification($template_key, $test_data);
|
||||
} else {
|
||||
throw new Exception('유효하지 않은 템플릿 타입입니다.');
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
$message = '테스트 알림이 성공적으로 발송되었습니다.';
|
||||
if (isset($result['debug_info'])) {
|
||||
$message .= '\n\n디버그 정보: ' . $result['debug_info'];
|
||||
}
|
||||
alert($message);
|
||||
} else {
|
||||
throw new Exception('알림 발송 실패: ' . $result['message']);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert('오류: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
goto_url('./notification_test.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 템플릿 목록 조회
|
||||
$email_templates = [];
|
||||
$sms_templates = [];
|
||||
|
||||
$email_sql = "SELECT template_key, template_name FROM order_mail_templates ORDER BY template_key ASC";
|
||||
$email_result = sql_query($email_sql);
|
||||
while ($row = sql_fetch_array($email_result)) {
|
||||
$email_templates[] = $row;
|
||||
}
|
||||
|
||||
$sms_sql = "SELECT template_key, template_name FROM order_sms_templates ORDER BY template_key ASC";
|
||||
$sms_result = sql_query($sms_sql);
|
||||
while ($row = sql_fetch_array($sms_result)) {
|
||||
$sms_templates[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
이메일 및 SMS 알림 템플릿의 실제 발송을 테스트할 수 있습니다.<br>
|
||||
개발 단계에서는 알림창으로 표시되며, 운영 단계에서는 실제 발송됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 알림 발송 테스트 -->
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<form name="ftest" method="post" onsubmit="return validateTestForm()">
|
||||
<input type="hidden" name="action" value="test_notification">
|
||||
|
||||
<table>
|
||||
<caption>알림 발송 테스트</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="template_type">템플릿 타입<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<select name="template_type" id="template_type" class="frm_input"
|
||||
onchange="updateTemplateList()" required>
|
||||
<option value="">선택하세요</option>
|
||||
<option value="customer">고객 알림</option>
|
||||
<option value="dealer">대리점 알림</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="template_key">템플릿<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<select name="template_key" id="template_key" class="frm_input"
|
||||
onchange="updateNotificationTypes()" required>
|
||||
<option value="">먼저 템플릿 타입을 선택하세요</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="notification_type">발송 방식<strong class="sound_only">필수</strong></label>
|
||||
</th>
|
||||
<td>
|
||||
<label><input type="radio" name="notification_type" value="email"
|
||||
onchange="updateRecipientField()"> 이메일</label>
|
||||
<label><input type="radio" name="notification_type" value="sms"
|
||||
onchange="updateRecipientField()"> SMS</label>
|
||||
<label><input type="radio" name="notification_type" value="both"
|
||||
onchange="updateRecipientField()"> 둘 다</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="test_recipient">테스트 수신자<strong class="sound_only">필수</strong></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="test_recipient" id="test_recipient" class="frm_input" required>
|
||||
<span class="frm_info" id="recipient_info">발송 방식을 먼저 선택하세요</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="테스트 발송" class="btn_submit">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 발송 모드 설정 -->
|
||||
<div class="tbl_frm01 tbl_wrap" style="margin-top: 30px;">
|
||||
<table>
|
||||
<caption>발송 모드 설정</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">현재 모드</th>
|
||||
<td>
|
||||
<?php
|
||||
// 개발/운영 모드 확인 (설정에서 가져오거나 기본값 사용)
|
||||
$notification_mode = 'development'; // 실제로는 설정에서 가져와야 함
|
||||
?>
|
||||
<strong><?php echo $notification_mode === 'development' ? '개발 모드 (알림창 표시)' : '운영 모드 (실제 발송)'; ?></strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">모드 설명</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li><strong>개발 모드:</strong> 실제 발송 대신 알림창으로 내용을 표시합니다.</li>
|
||||
<li><strong>운영 모드:</strong> 실제 이메일/SMS를 발송합니다.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 템플릿 변수 참조 -->
|
||||
<div class="tbl_head01 tbl_wrap" style="margin-top: 30px;">
|
||||
<table>
|
||||
<caption>사용 가능한 템플릿 변수</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">변수명</th>
|
||||
<th scope="col">설명</th>
|
||||
<th scope="col">예시값</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>{customer_name}</code></td>
|
||||
<td>고객명</td>
|
||||
<td>홍길동</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{estimate_id}</code></td>
|
||||
<td>견적 번호</td>
|
||||
<td>12345</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{estimate_subject}</code></td>
|
||||
<td>견적 제목</td>
|
||||
<td>아파트 창호 교체 견적</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{total_amount}</code></td>
|
||||
<td>총 금액</td>
|
||||
<td>1,500,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{visit_fee}</code></td>
|
||||
<td>방문 비용</td>
|
||||
<td>50,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{dealer_name}</code></td>
|
||||
<td>대리점명</td>
|
||||
<td>김대리점</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{construction_date}</code></td>
|
||||
<td>시공 예정일</td>
|
||||
<td>2024-02-15</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{account_info}</code></td>
|
||||
<td>계좌 정보</td>
|
||||
<td>국민은행 123-456-789012</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 템플릿 데이터
|
||||
const emailTemplates = <?php echo json_encode($email_templates); ?>;
|
||||
const smsTemplates = <?php echo json_encode($sms_templates); ?>;
|
||||
|
||||
// 템플릿 타입 변경 시 템플릿 목록 업데이트
|
||||
function updateTemplateList() {
|
||||
const templateType = document.getElementById('template_type').value;
|
||||
const templateSelect = document.getElementById('template_key');
|
||||
|
||||
// 기존 옵션 제거
|
||||
templateSelect.innerHTML = '<option value="">템플릿을 선택하세요</option>';
|
||||
|
||||
if (templateType === 'customer' || templateType === 'dealer') {
|
||||
// 이메일과 SMS 템플릿 모두 추가
|
||||
emailTemplates.forEach(template => {
|
||||
const option = document.createElement('option');
|
||||
option.value = template.template_key;
|
||||
option.textContent = `[이메일] ${template.template_name}`;
|
||||
templateSelect.appendChild(option);
|
||||
});
|
||||
|
||||
smsTemplates.forEach(template => {
|
||||
const option = document.createElement('option');
|
||||
option.value = template.template_key;
|
||||
option.textContent = `[SMS] ${template.template_name}`;
|
||||
templateSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 발송 방식 초기화
|
||||
document.querySelectorAll('input[name="notification_type"]').forEach(radio => {
|
||||
radio.checked = false;
|
||||
});
|
||||
updateRecipientField();
|
||||
}
|
||||
|
||||
// 템플릿 선택 시 발송 방식 업데이트
|
||||
function updateNotificationTypes() {
|
||||
const templateKey = document.getElementById('template_key').value;
|
||||
|
||||
if (templateKey) {
|
||||
// 선택된 템플릿이 이메일인지 SMS인지 확인
|
||||
const isEmailTemplate = emailTemplates.some(t => t.template_key === templateKey);
|
||||
const isSmsTemplate = smsTemplates.some(t => t.template_key === templateKey);
|
||||
|
||||
// 해당하는 발송 방식만 활성화
|
||||
document.querySelector('input[value="email"]').disabled = !isEmailTemplate;
|
||||
document.querySelector('input[value="sms"]').disabled = !isSmsTemplate;
|
||||
document.querySelector('input[value="both"]').disabled = !(isEmailTemplate && isSmsTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
// 발송 방식 변경 시 수신자 필드 업데이트
|
||||
function updateRecipientField() {
|
||||
const notificationType = document.querySelector('input[name="notification_type"]:checked');
|
||||
const recipientField = document.getElementById('test_recipient');
|
||||
const recipientInfo = document.getElementById('recipient_info');
|
||||
|
||||
if (!notificationType) {
|
||||
recipientField.placeholder = '';
|
||||
recipientInfo.textContent = '발송 방식을 먼저 선택하세요';
|
||||
return;
|
||||
}
|
||||
|
||||
switch (notificationType.value) {
|
||||
case 'email':
|
||||
recipientField.placeholder = 'test@example.com';
|
||||
recipientInfo.textContent = '테스트 이메일 주소를 입력하세요';
|
||||
break;
|
||||
case 'sms':
|
||||
recipientField.placeholder = '01012345678';
|
||||
recipientInfo.textContent = '테스트 전화번호를 입력하세요 (하이픈 없이)';
|
||||
break;
|
||||
case 'both':
|
||||
recipientField.placeholder = 'test@example.com 또는 01012345678';
|
||||
recipientInfo.textContent = '이메일 주소 또는 전화번호를 입력하세요';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 폼 유효성 검사
|
||||
function validateTestForm() {
|
||||
const templateType = document.getElementById('template_type').value;
|
||||
const templateKey = document.getElementById('template_key').value;
|
||||
const notificationType = document.querySelector('input[name="notification_type"]:checked');
|
||||
const testRecipient = document.getElementById('test_recipient').value.trim();
|
||||
|
||||
if (!templateType || !templateKey || !notificationType || !testRecipient) {
|
||||
alert('모든 필수 항목을 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 수신자 유효성 검증
|
||||
if (notificationType.value === 'email' || notificationType.value === 'both') {
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(testRecipient)) {
|
||||
alert('유효한 이메일 주소를 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationType.value === 'sms') {
|
||||
const phoneNumber = testRecipient.replace(/[^0-9]/g, '');
|
||||
if (!/^01[0-9]{8,9}$/.test(phoneNumber)) {
|
||||
alert('유효한 전화번호를 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return confirm('테스트 알림을 발송하시겠습니까?');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
$sub_menu = '800800';
|
||||
include_once('./_common.php');
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
// 결제 상태 업데이트 처리
|
||||
if ($_POST['mode'] == 'update_payment') {
|
||||
$wr_id = (int) $_POST['wr_id'];
|
||||
$payment_status = sql_real_escape_string($_POST['payment_status']);
|
||||
$payment_memo = sql_real_escape_string($_POST['payment_memo']);
|
||||
|
||||
// 결제 상태에 따른 견적 상태 결정
|
||||
$estimate_status_map = [
|
||||
'pending' => '견적채택',
|
||||
'deposit_paid' => '입금확인',
|
||||
'interim_pending' => '중도금입금예정',
|
||||
'interim_paid' => '중도금입금완료',
|
||||
'final_pending' => '잔금입금예정',
|
||||
'final_paid' => '시공완료'
|
||||
];
|
||||
|
||||
$new_estimate_status = $estimate_status_map[$payment_status] ?? '견적채택';
|
||||
|
||||
sql_query("UPDATE estimate SET
|
||||
status = '{$new_estimate_status}',
|
||||
extra_2 = '{$payment_status}',
|
||||
extra_4 = '{$payment_memo}',
|
||||
updated_at = NOW()
|
||||
WHERE wr_id = '{$wr_id}'");
|
||||
|
||||
// 게시판 상태도 업데이트
|
||||
sql_query("UPDATE g5_write_order SET wr_1 = '{$new_estimate_status}' WHERE wr_id = '{$wr_id}'");
|
||||
|
||||
alert('결제 상태가 업데이트되었습니다.', './payment_manager.php');
|
||||
}
|
||||
|
||||
// 결제 대기 중인 견적 목록 조회
|
||||
$sql = "SELECT e.*, w.wr_subject, w.wr_name, w.wr_datetime,
|
||||
(SELECT mb_name FROM g5_member WHERE mb_id = e.extra_4) as selected_dealer_name
|
||||
FROM estimate e
|
||||
JOIN g5_write_order w ON e.wr_id = w.wr_id
|
||||
WHERE e.status IN ('견적채택', '입금확인', '중도금입금예정', '중도금입금완료', '잔금입금예정')
|
||||
AND e.is_deleted = 0
|
||||
ORDER BY
|
||||
CASE e.status
|
||||
WHEN '견적채택' THEN 1
|
||||
WHEN '중도금입금예정' THEN 2
|
||||
WHEN '잔금입금예정' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
e.updated_at DESC";
|
||||
$result = sql_query($sql);
|
||||
|
||||
$g5['title'] = '결제 관리';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>견적 선택 후 결제 진행 상황을 관리합니다.</p>
|
||||
</div>
|
||||
|
||||
<section id="anc_payment_manager">
|
||||
<h2>결제 관리 목록</h2>
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>결제 진행 현황</caption>
|
||||
<colgroup>
|
||||
<col style="width:80px">
|
||||
<col>
|
||||
<col style="width:100px">
|
||||
<col style="width:100px">
|
||||
<col style="width:120px">
|
||||
<col style="width:100px">
|
||||
<col style="width:100px">
|
||||
<col style="width:80px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">번호</th>
|
||||
<th scope="col">제목</th>
|
||||
<th scope="col">고객명</th>
|
||||
<th scope="col">선택된 대리점</th>
|
||||
<th scope="col">견적 상태</th>
|
||||
<th scope="col">결제 상태</th>
|
||||
<th scope="col">결제 금액</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$num = 1;
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$payment_status = $row['extra_2'] ?: 'pending';
|
||||
$deposit_amount = $row['extra_3'] ?: 0;
|
||||
|
||||
$payment_status_text = [
|
||||
'pending' => '계약금 대기',
|
||||
'deposit_paid' => '계약금 완료',
|
||||
'interim_pending' => '중도금 대기',
|
||||
'interim_paid' => '중도금 완료',
|
||||
'final_pending' => '잔금 대기',
|
||||
'final_paid' => '잔금 완료'
|
||||
];
|
||||
|
||||
$status_class = [
|
||||
'pending' => 'status-pending',
|
||||
'deposit_paid' => 'status-paid',
|
||||
'interim_pending' => 'status-pending',
|
||||
'interim_paid' => 'status-paid',
|
||||
'final_pending' => 'status-pending',
|
||||
'final_paid' => 'status-completed'
|
||||
];
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo $num++; ?></td>
|
||||
<td>
|
||||
<a href="<?php echo G5_BBS_URL; ?>/board.php?bo_table=order&wr_id=<?php echo $row['wr_id']; ?>">
|
||||
<?php echo htmlspecialchars($row['wr_subject']); ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?php echo $row['wr_name']; ?></td>
|
||||
<td><?php echo $row['selected_dealer_name'] ?: '-'; ?></td>
|
||||
<td>
|
||||
<span class="estimate-status">
|
||||
<?php echo get_status_display_name($row['status']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="payment-status <?php echo $status_class[$payment_status] ?? ''; ?>">
|
||||
<?php echo $payment_status_text[$payment_status] ?? $payment_status; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo $deposit_amount ? number_format($deposit_amount) . '원' : '-'; ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn_03"
|
||||
onclick="editPayment(<?php echo $row['wr_id']; ?>, '<?php echo $payment_status; ?>')">관리</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
<?php if (sql_num_rows($result) == 0) { ?>
|
||||
<tr>
|
||||
<td colspan="8" class="empty_table">결제 대기 중인 견적이 없습니다.</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 결제 관리 모달 -->
|
||||
<div id="paymentModal"
|
||||
style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:white; border:2px solid #ccc; padding:20px; z-index:1000; width:500px;">
|
||||
<h3>결제 상태 관리</h3>
|
||||
<form name="paymentForm" method="post" action="./payment_manager.php">
|
||||
<input type="hidden" name="mode" value="update_payment">
|
||||
<input type="hidden" name="wr_id" id="modal_wr_id">
|
||||
|
||||
<table class="tbl_frm01">
|
||||
<tr>
|
||||
<th>결제 상태</th>
|
||||
<td>
|
||||
<select name="payment_status" id="modal_payment_status">
|
||||
<option value="pending">계약금 대기</option>
|
||||
<option value="deposit_paid">계약금 완료</option>
|
||||
<option value="interim_pending">중도금 대기</option>
|
||||
<option value="interim_paid">중도금 완료</option>
|
||||
<option value="final_pending">잔금 대기</option>
|
||||
<option value="final_paid">잔금 완료 (시공완료)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>관리자 메모</th>
|
||||
<td>
|
||||
<textarea name="payment_memo" id="modal_payment_memo" rows="3" cols="50"
|
||||
placeholder="입금 확인일, 특이사항 등"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="text-align:center; margin-top:20px;">
|
||||
<input type="submit" value="저장" class="btn btn_02">
|
||||
<button type="button" onclick="closePaymentModal()" class="btn btn_03">취소</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="paymentModalOverlay"
|
||||
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:999;"
|
||||
onclick="closePaymentModal()"></div>
|
||||
|
||||
<style>
|
||||
.status-pending {
|
||||
color: #ff6600;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-paid {
|
||||
color: #0066cc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
color: #009900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.estimate-status {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.payment-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty_table {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function editPayment(wr_id, current_status) {
|
||||
document.getElementById('modal_wr_id').value = wr_id;
|
||||
document.getElementById('modal_payment_status').value = current_status;
|
||||
document.getElementById('paymentModal').style.display = 'block';
|
||||
document.getElementById('paymentModalOverlay').style.display = 'block';
|
||||
}
|
||||
|
||||
function closePaymentModal() {
|
||||
document.getElementById('paymentModal').style.display = 'none';
|
||||
document.getElementById('paymentModalOverlay').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
$sub_menu = '800900'; // 메뉴 코드 (적절히 수정 필요)
|
||||
include_once('./_common.php');
|
||||
|
||||
$g5['title'] = '전문가 방문 예약 기능 사용법';
|
||||
include_once(G5_ADMIN_PATH.'/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
이 페이지는 사이트의 어떤 페이지에서든 <strong>전문가 방문 예약</strong> 및 <strong>나의 예약 확인</strong> 기능을 추가하는 방법을 보여주는 예제입니다.<br>
|
||||
아래 코드 한 줄만 포함하면, 버튼과 팝업 기능이 모두 활성화됩니다.
|
||||
</p>
|
||||
<pre><code><?php include_once(G5_ADMIN_PATH . '/order_manage/components/_expert_visit_popups.php'); ?></code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [시작] 실제 작동 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<div style="text-align:center; padding: 50px 20px; background-color:#f5f5f5; border-radius:10px; margin: 20px 0;">
|
||||
<h3 style="margin-bottom:15px;">전문가 방문이 필요하신가요?</h3>
|
||||
<p style="margin-bottom:25px; color:#666;">버튼을 눌러 간편하게 방문을 신청하거나, 기존 예약을 확인/취소할 수 있습니다.</p>
|
||||
|
||||
<!-- 1. 버튼들 -->
|
||||
<div class="expert-buttons">
|
||||
<!-- wr_id는 게시판 글 ID입니다. 없으면 0 또는 생략 가능합니다. -->
|
||||
<button type="button" class="expert-request-btn-main" onclick="openExpertVisitPopup(0)">
|
||||
<i class="fa fa-calendar-check-o"></i> 전문가 방문 예약하기
|
||||
</button>
|
||||
<button type="button" class="my-reservation-btn" onclick="openMyExpertReservationsPopup()">
|
||||
<i class="fa fa-list"></i> 나의 예약 현황
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 2. 팝업 파일들 포함 (이 코드 한 줄이면 모든 팝업 기능이 로드됩니다)
|
||||
include_once(G5_ADMIN_PATH . '/order_manage/components/_expert_visit_popups.php');
|
||||
?>
|
||||
|
||||
<!-- 3. 스타일 (CSS 파일에 추가 권장) -->
|
||||
<style>
|
||||
/* 버튼 스타일 */
|
||||
.expert-buttons { display: flex; justify-content: center; gap: 15px; flex-wrap: wrap; }
|
||||
.expert-request-btn-main, .my-reservation-btn {
|
||||
color: white; border: none; padding: 15px 30px; border-radius: 25px;
|
||||
font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.3s ease;
|
||||
}
|
||||
.expert-request-btn-main {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #f98e5a 100%);
|
||||
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
.expert-request-btn-main:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4); }
|
||||
.my-reservation-btn { background: #6c757d; }
|
||||
.my-reservation-btn:hover { background: #5a6268; }
|
||||
.expert-request-btn-main i, .my-reservation-btn i { margin-right: 8px; }
|
||||
</style>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [끝] 실제 작동 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<h3>📌 적용 방법</h3>
|
||||
<p>위의 예제 코드를 복사하여 원하는 페이지(예: <code>tail.php</code>, <code>index.php</code> 등)에 붙여넣으세요.</p>
|
||||
<p><strong>주의사항:</strong> <code>G5_ADMIN_PATH</code> 상수는 그누보드 설정에 따라 다를 수 있으므로, 실제 경로(예: <code>/adm</code>)로 직접 입력해야 할 수도 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH.'/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
$sub_menu = "800600";
|
||||
include_once("./_common.php");
|
||||
|
||||
auth_check_menu($auth, $sub_menu, "r");
|
||||
|
||||
$id = (int) $_GET['id'];
|
||||
$template = sql_fetch("SELECT * FROM order_sms_templates WHERE id = {$id}");
|
||||
|
||||
if (!$template) {
|
||||
alert('템플릿을 찾을 수 없습니다.', './sms_templates.php');
|
||||
}
|
||||
|
||||
$g5['title'] = "SMS 템플릿 수정";
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
$variables = json_decode($template['variables'], true);
|
||||
?>
|
||||
|
||||
<div class="local_ov01 local_ov">
|
||||
SMS 템플릿 수정 - <?php echo htmlspecialchars($template['template_name']); ?>
|
||||
</div>
|
||||
|
||||
<form name="templateForm" method="post" action="sms_template_update.php">
|
||||
<input type="hidden" name="id" value="<?php echo $template['id']; ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>SMS 템플릿 수정</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">템플릿명</th>
|
||||
<td>
|
||||
<input type="text" name="template_name"
|
||||
value="<?php echo htmlspecialchars($template['template_name']); ?>" class="frm_input"
|
||||
style="width:100%;" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">SMS 내용</th>
|
||||
<td>
|
||||
<textarea name="content" id="sms_content" class="frm_input" style="width:100%; height:150px;"
|
||||
onkeyup="checkSMSLength();"
|
||||
required><?php echo htmlspecialchars($template['content']); ?></textarea>
|
||||
<div id="sms_length_info">
|
||||
<span id="current_length">0</span> / 80 바이트
|
||||
(<span id="current_chars">0</span> / 40 글자)
|
||||
<span id="length_warning" style="color: red; display: none;">SMS 길이 초과!</span>
|
||||
</div>
|
||||
<div class="frm_info">한글 1글자 = 2바이트, 영문/숫자 1글자 = 1바이트</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">사용 가능한 변수</th>
|
||||
<td>
|
||||
<?php if (is_array($variables)) { ?>
|
||||
<?php foreach ($variables as $var) { ?>
|
||||
<span class="variable-tag"
|
||||
onclick="insertVariable('{<?php echo $var; ?>}')">{<?php echo $var; ?>}</span>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<div class="frm_info">변수를 클릭하면 SMS 내용에 삽입됩니다.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="저장" class="btn_submit">
|
||||
<a href="sms_templates.php" class="btn btn_02">목록</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.variable-tag {
|
||||
display: inline-block;
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
padding: 2px 8px;
|
||||
margin: 2px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.variable-tag:hover {
|
||||
background: #c8e6c9;
|
||||
}
|
||||
|
||||
#sms_length_info {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function checkSMSLength() {
|
||||
const content = document.getElementById('sms_content').value;
|
||||
let byteLength = 0;
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content.charAt(i);
|
||||
byteLength += (escape(char).length > 4) ? 2 : 1;
|
||||
}
|
||||
|
||||
document.getElementById('current_length').textContent = byteLength;
|
||||
document.getElementById('current_chars').textContent = content.length;
|
||||
|
||||
const warning = document.getElementById('length_warning');
|
||||
if (byteLength > 80) {
|
||||
warning.style.display = 'inline';
|
||||
} else {
|
||||
warning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function insertVariable(variable) {
|
||||
const textarea = document.getElementById('sms_content');
|
||||
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);
|
||||
|
||||
checkSMSLength();
|
||||
}
|
||||
|
||||
// 페이지 로드 시 길이 체크
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
checkSMSLength();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
$sub_menu = "800600";
|
||||
include_once("./_common.php");
|
||||
|
||||
auth_check_menu($auth, $sub_menu, "w");
|
||||
|
||||
if ($_POST) {
|
||||
$id = (int) $_POST['id'];
|
||||
$template_name = sql_real_escape_string($_POST['template_name']);
|
||||
$content = sql_real_escape_string($_POST['content']);
|
||||
|
||||
$sql = "UPDATE order_sms_templates SET
|
||||
template_name = '{$template_name}',
|
||||
content = '{$content}',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$id}";
|
||||
|
||||
sql_query($sql);
|
||||
|
||||
alert('SMS 템플릿이 수정되었습니다.', './sms_templates.php');
|
||||
} else {
|
||||
alert('잘못된 접근입니다.', './sms_templates.php');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
$sub_menu = "800600";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = 'SMS 템플릿 관리';
|
||||
|
||||
// 액션 처리
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
$id = (int) ($_REQUEST['id'] ?? 0);
|
||||
|
||||
// POST 요청 처리 (생성, 수정, 삭제)
|
||||
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
try {
|
||||
if ($action === 'save') {
|
||||
$template_key = clean_xss_tags($_POST['template_key'] ?? '');
|
||||
$template_name = clean_xss_tags($_POST['template_name'] ?? '');
|
||||
$content = clean_xss_tags($_POST['content'] ?? '');
|
||||
$variables = clean_xss_tags($_POST['variables'] ?? '');
|
||||
|
||||
if (!$template_key || !$template_name || !$content) {
|
||||
throw new Exception('필수 항목을 모두 입력해주세요.');
|
||||
}
|
||||
|
||||
// SMS 길이 제한 확인 (한글 기준 90자, 영문 기준 160자)
|
||||
if (mb_strlen($content, 'UTF-8') > 90) {
|
||||
throw new Exception('SMS 내용은 90자를 초과할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 템플릿 키 중복 확인 (수정 시 제외)
|
||||
if ($id === 0) {
|
||||
$existing = sql_fetch("SELECT id FROM order_sms_templates WHERE template_key = '{$template_key}'");
|
||||
if ($existing) {
|
||||
throw new Exception('이미 존재하는 템플릿 키입니다.');
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'template_key' => $template_key,
|
||||
'template_name' => $template_name,
|
||||
'content' => $content,
|
||||
'variables' => $variables,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
// 수정
|
||||
$set_clauses = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$set_clauses[] = "`{$key}` = '" . sql_real_escape_string($value) . "'";
|
||||
}
|
||||
$sql = "UPDATE order_sms_templates SET " . implode(', ', $set_clauses) . " WHERE id = '{$id}'";
|
||||
sql_query($sql);
|
||||
$message = 'SMS 템플릿이 수정되었습니다.';
|
||||
} else {
|
||||
// 생성
|
||||
$data['created_at'] = date('Y-m-d H:i:s');
|
||||
$fields = array_keys($data);
|
||||
$values = array_values($data);
|
||||
$fields_str = '`' . implode('`, `', $fields) . '`';
|
||||
$values_str = "'" . implode("', '", array_map('sql_real_escape_string', $values)) . "'";
|
||||
$sql = "INSERT INTO order_sms_templates ({$fields_str}) VALUES ({$values_str})";
|
||||
sql_query($sql);
|
||||
$message = 'SMS 템플릿이 생성되었습니다.';
|
||||
}
|
||||
|
||||
alert($message, './sms_templates.php');
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
if ($id > 0) {
|
||||
sql_query("DELETE FROM order_sms_templates WHERE id = '{$id}'");
|
||||
alert('SMS 템플릿이 삭제되었습니다.', './sms_templates.php');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert('오류: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
goto_url('./sms_templates.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// GET 요청 처리 (목록, 편집 폼)
|
||||
$mode = isset($_GET['mode']) ? clean_xss_tags($_GET['mode']) : 'list';
|
||||
$template = null;
|
||||
|
||||
if ($mode === 'edit' && $id > 0) {
|
||||
$template = sql_fetch("SELECT * FROM order_sms_templates WHERE id = '{$id}'");
|
||||
if (!$template) {
|
||||
alert('존재하지 않는 템플릿입니다.', './sms_templates.php');
|
||||
}
|
||||
}
|
||||
|
||||
// 템플릿 목록 조회
|
||||
$templates = [];
|
||||
$sql = "SELECT * FROM order_sms_templates ORDER BY template_key ASC";
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$templates[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
SMS 알림에 사용되는 템플릿을 관리합니다.<br>
|
||||
SMS는 90자 이내로 작성해야 하며, 변수를 사용하여 동적인 내용을 삽입할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($mode === 'list'): ?>
|
||||
<!-- 템플릿 목록 -->
|
||||
<div class="local_ov01 local_ov">
|
||||
<span class="btn_ov01">
|
||||
<span class="ov_txt">전체 템플릿 </span>
|
||||
<span class="ov_num"><?php echo count($templates); ?>개</span>
|
||||
</span>
|
||||
<a href="?mode=edit" class="btn btn_02">새 템플릿 추가</a>
|
||||
</div>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">템플릿 키</th>
|
||||
<th scope="col">템플릿명</th>
|
||||
<th scope="col">내용</th>
|
||||
<th scope="col">길이</th>
|
||||
<th scope="col">변수</th>
|
||||
<th scope="col">수정일</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($templates)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="empty_table">등록된 템플릿이 없습니다.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($templates as $tpl): ?>
|
||||
<tr>
|
||||
<td class="td_left">
|
||||
<code><?php echo get_text($tpl['template_key']); ?></code>
|
||||
</td>
|
||||
<td class="td_left">
|
||||
<strong><?php echo get_text($tpl['template_name']); ?></strong>
|
||||
</td>
|
||||
<td class="td_left">
|
||||
<div style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<?php echo get_text($tpl['content']); ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="td_num">
|
||||
<?php
|
||||
$length = mb_strlen($tpl['content'], 'UTF-8');
|
||||
$color = $length > 90 ? 'red' : ($length > 70 ? 'orange' : 'green');
|
||||
?>
|
||||
<span style="color: <?php echo $color; ?>"><?php echo $length; ?>/90자</span>
|
||||
</td>
|
||||
<td class="td_left">
|
||||
<?php
|
||||
$variables = json_decode($tpl['variables'], true);
|
||||
if (is_array($variables)) {
|
||||
echo '<small>' . implode(', ', array_map(function ($var) {
|
||||
return '{' . $var . '}'; }, $variables)) . '</small>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="td_datetime">
|
||||
<?php echo $tpl['updated_at'] ? date('Y-m-d H:i', strtotime($tpl['updated_at'])) : '-'; ?>
|
||||
</td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<a href="?mode=edit&id=<?php echo $tpl['id']; ?>" class="btn btn_03">수정</a>
|
||||
<a href="javascript:void(0);" onclick="previewTemplate(<?php echo $tpl['id']; ?>)"
|
||||
class="btn btn_04">미리보기</a>
|
||||
<a href="javascript:void(0);" onclick="deleteTemplate(<?php echo $tpl['id']; ?>)"
|
||||
class="btn btn_02">삭제</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- 템플릿 편집 폼 -->
|
||||
<div class="local_desc02 local_desc">
|
||||
<p>
|
||||
<a href="./sms_templates.php" class="btn btn_01">목록으로 돌아가기</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form name="ftemplate" method="post" onsubmit="return validateForm()">
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="id" value="<?php echo $template['id'] ?? 0; ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $template ? '템플릿 수정' : '새 템플릿 추가'; ?></caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="template_key">템플릿 키<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<input type="text" name="template_key" id="template_key"
|
||||
value="<?php echo get_text($template['template_key'] ?? ''); ?>" class="frm_input"
|
||||
maxlength="100" required <?php echo $template ? 'readonly' : ''; ?>>
|
||||
<span class="frm_info">영문, 숫자, 언더스코어만 사용 가능 (예: customer_request_complete)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="template_name">템플릿명<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<input type="text" name="template_name" id="template_name"
|
||||
value="<?php echo get_text($template['template_name'] ?? ''); ?>" class="frm_input"
|
||||
maxlength="255" required>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="content">SMS 내용<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<textarea name="content" id="content" rows="5" class="frm_input" maxlength="90" required
|
||||
oninput="updateCharCount()"
|
||||
onkeyup="updateCharCount()"><?php echo get_text($template['content'] ?? ''); ?></textarea>
|
||||
<div class="char-count">
|
||||
<span id="charCount">0</span>/90자
|
||||
<span id="charWarning" style="color: red; display: none;">90자를 초과했습니다!</span>
|
||||
</div>
|
||||
<span class="frm_info">변수는 {변수명} 형태로 입력. 예: {customer_name}님, 안녕하세요.</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="variables">사용 가능한 변수</label></th>
|
||||
<td>
|
||||
<input type="text" name="variables" id="variables"
|
||||
value="<?php echo get_text($template['variables'] ?? ''); ?>" class="frm_input"
|
||||
placeholder='["customer_name", "estimate_id"]'>
|
||||
<span class="frm_info">JSON 배열 형태로 입력 (예: ["customer_name", "estimate_id"])</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="<?php echo $template ? '수정' : '등록'; ?>" class="btn_submit">
|
||||
<a href="./sms_templates.php" class="btn_cancel">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 미리보기 모달 -->
|
||||
<div id="previewModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>SMS 템플릿 미리보기</h3>
|
||||
<span class="close" onclick="closePreview()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="previewContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
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: #fefefe;
|
||||
margin: 15% auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 500px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 15px 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.sms-preview {
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 문자 수 카운트 업데이트
|
||||
function updateCharCount() {
|
||||
const content = document.getElementById('content');
|
||||
const charCount = document.getElementById('charCount');
|
||||
const charWarning = document.getElementById('charWarning');
|
||||
|
||||
if(!content || !charCount || !charWarning){
|
||||
console.log(document.getElementById('content'))
|
||||
return;
|
||||
}
|
||||
const length = content && content.value ? content.value.length :-1;
|
||||
if(charCount) {
|
||||
charCount.textContent = charCount && charCount.textContent ? length : -1;
|
||||
}
|
||||
|
||||
if (length > 90) {
|
||||
charCount.style.color = 'red';
|
||||
charWarning.style.display = 'inline';
|
||||
} else if (length > 70) {
|
||||
charCount.style.color = 'orange';
|
||||
charWarning.style.display = 'none';
|
||||
} else {
|
||||
charCount.style.color = 'green';
|
||||
charWarning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 문자 수 카운트 초기화
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
updateCharCount();
|
||||
});
|
||||
|
||||
// 폼 유효성 검사
|
||||
function validateForm() {
|
||||
const templateKey = document.getElementById('template_key').value.trim();
|
||||
const templateName = document.getElementById('template_name').value.trim();
|
||||
const content = document.getElementById('content').value.trim();
|
||||
|
||||
if (!templateKey || !templateName || !content) {
|
||||
alert('필수 항목을 모두 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 템플릿 키 형식 검사
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(templateKey)) {
|
||||
alert('템플릿 키는 영문, 숫자, 언더스코어만 사용할 수 있습니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// SMS 길이 제한 검사
|
||||
if (content.length > 90) {
|
||||
alert('SMS 내용은 90자를 초과할 수 없습니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 변수 JSON 형식 검사
|
||||
const variables = document.getElementById('variables').value.trim();
|
||||
if (variables) {
|
||||
try {
|
||||
JSON.parse(variables);
|
||||
} catch (e) {
|
||||
alert('변수는 올바른 JSON 배열 형태로 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 템플릿 삭제
|
||||
function deleteTemplate(id) {
|
||||
if (!confirm('정말로 이 템플릿을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.innerHTML = `
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="${id}">
|
||||
`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// 템플릿 미리보기
|
||||
function previewTemplate(id) {
|
||||
fetch('sms_templates_ajax.php?action=preview&id=' + id)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const length = data.data.content.length;
|
||||
const lengthColor = length > 90 ? 'red' : (length > 70 ? 'orange' : 'green');
|
||||
|
||||
document.getElementById('previewContent').innerHTML = `
|
||||
<div class="sms-preview">${data.data.content}</div>
|
||||
<p><strong>길이:</strong> <span style="color: ${lengthColor}">${length}/90자</span></p>
|
||||
<p><strong>사용 가능한 변수:</strong> ${data.data.variables || '없음'}</p>
|
||||
`;
|
||||
document.getElementById('previewModal').style.display = 'block';
|
||||
} else {
|
||||
alert('미리보기를 불러올 수 없습니다: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('미리보기 중 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
|
||||
// 미리보기 닫기
|
||||
function closePreview() {
|
||||
document.getElementById('previewModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById('previewModal');
|
||||
if (event.target === modal) {
|
||||
closePreview();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
|
||||
// AJAX 요청만 처리
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'GET 요청만 허용됩니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = isset($_GET['action']) ? clean_xss_tags($_GET['action']) : '';
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($action === 'preview' && $id > 0) {
|
||||
$template = sql_fetch("SELECT * FROM order_sms_templates WHERE id = '{$id}'");
|
||||
|
||||
if ($template) {
|
||||
// 변수들을 예시 값으로 치환
|
||||
$variables = json_decode($template['variables'], true);
|
||||
$content = $template['content'];
|
||||
|
||||
if (is_array($variables)) {
|
||||
foreach ($variables as $var) {
|
||||
$placeholder = '{' . $var . '}';
|
||||
$example_value = '[' . $var . ']'; // 예: [customer_name]
|
||||
$content = str_replace($placeholder, $example_value, $content);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'content' => $content,
|
||||
'variables' => $template['variables']
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => '템플릿을 찾을 수 없습니다.']);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => '잘못된 요청입니다.']);
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
$sub_menu = "800200";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '견적 통계 및 리포트';
|
||||
|
||||
// EstimateManager 로드
|
||||
require_once G5_PATH . '/adm/order_manage/classes/EstimateManager.class.php';
|
||||
$estimate_manager = new EstimateManager();
|
||||
|
||||
// 필터 파라미터
|
||||
$date_from = isset($_GET['date_from']) ? clean_xss_tags($_GET['date_from']) : date('Y-m-01'); // 이번 달 첫째 날
|
||||
$date_to = isset($_GET['date_to']) ? clean_xss_tags($_GET['date_to']) : date('Y-m-d'); // 오늘
|
||||
$report_type = isset($_GET['report_type']) ? clean_xss_tags($_GET['report_type']) : 'overview';
|
||||
|
||||
// CSV 내보내기 처리
|
||||
if (isset($_GET['export']) && $_GET['export'] === 'csv') {
|
||||
$export_type = clean_xss_tags($_GET['export_type'] ?? 'estimates');
|
||||
$csv_data = $estimate_manager->exportStatisticsCSV($export_type, $date_from, $date_to);
|
||||
|
||||
if ($csv_data) {
|
||||
$filename = "order_statistics_{$export_type}_" . date('Y-m-d') . ".csv";
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
|
||||
// UTF-8 BOM 추가 (엑셀에서 한글 깨짐 방지)
|
||||
echo "\xEF\xBB\xBF";
|
||||
echo $csv_data;
|
||||
exit;
|
||||
} else {
|
||||
alert('CSV 내보내기에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 데이터 조회
|
||||
$estimate_stats = $estimate_manager->getEstimateStatistics($date_from, $date_to);
|
||||
$dealer_performance = $estimate_manager->getDealerPerformance($date_from, $date_to);
|
||||
$revenue_stats = $estimate_manager->getRevenueStatistics($date_from, $date_to);
|
||||
$expert_visit_stats = $estimate_manager->getExpertVisitStatistics($date_from, $date_to);
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
견적 요청 현황, 대리점 성과, 매출 분석, 전문가 방문 통계 등 종합적인 비즈니스 인사이트를 제공합니다.<br>
|
||||
데이터를 CSV 파일로 내보내어 상세 분석이 가능합니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 필터 및 내보내기 영역 -->
|
||||
<div class="local_sch01 local_sch">
|
||||
<form name="fsearch" method="get">
|
||||
<fieldset>
|
||||
<legend>기간 설정</legend>
|
||||
|
||||
<label for="date_from" class="sound_only">시작일</label>
|
||||
<input type="date" name="date_from" id="date_from" value="<?php echo $date_from; ?>" class="frm_input">
|
||||
~
|
||||
<label for="date_to" class="sound_only">종료일</label>
|
||||
<input type="date" name="date_to" id="date_to" value="<?php echo $date_to; ?>" class="frm_input">
|
||||
|
||||
<input type="submit" value="조회" class="btn_submit">
|
||||
|
||||
<div style="float: right;">
|
||||
<select id="export_type" class="frm_input">
|
||||
<option value="estimates">견적 통계</option>
|
||||
<option value="dealers">대리점 성과</option>
|
||||
<option value="revenue">매출 현황</option>
|
||||
<option value="expert_visits">전문가 방문</option>
|
||||
</select>
|
||||
<button type="button" onclick="exportCSV()" class="btn btn_02">CSV 내보내기</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 탭 메뉴 -->
|
||||
<div class="tab-menu">
|
||||
<button class="tab-btn active" onclick="showTab('overview')">전체 개요</button>
|
||||
<button class="tab-btn" onclick="showTab('estimates')">견적 통계</button>
|
||||
<button class="tab-btn" onclick="showTab('dealers')">대리점 성과</button>
|
||||
<button class="tab-btn" onclick="showTab('revenue')">매출 분석</button>
|
||||
<button class="tab-btn" onclick="showTab('expert_visits')">전문가 방문</button>
|
||||
</div>
|
||||
|
||||
<!-- 전체 개요 탭 -->
|
||||
<div id="overview-tab" class="tab-content active">
|
||||
<div class="stats-grid">
|
||||
<!-- 견적 현황 카드 -->
|
||||
<div class="stats-card">
|
||||
<h3>견적 현황</h3>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($estimate_stats['total_statistics']['total_estimates']); ?></div>
|
||||
<div class="stats-label">총 견적 요청</div>
|
||||
<div class="stats-detail">
|
||||
완료: <?php echo number_format($estimate_stats['total_statistics']['completed_estimates']); ?>건 |
|
||||
선택: <?php echo number_format($estimate_stats['total_statistics']['selected_estimates']); ?>건 |
|
||||
전환율: <?php echo $estimate_stats['total_statistics']['conversion_rate']; ?>%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 매출 현황 카드 -->
|
||||
<div class="stats-card">
|
||||
<h3>매출 현황</h3>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($revenue_stats['total_revenue']['total_revenue'] ?? 0); ?>원</div>
|
||||
<div class="stats-label">총 매출</div>
|
||||
<div class="stats-detail">
|
||||
주문: <?php echo number_format($revenue_stats['total_revenue']['total_orders'] ?? 0); ?>건 |
|
||||
평균: <?php echo number_format($revenue_stats['total_revenue']['avg_order_value'] ?? 0); ?>원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 대리점 현황 카드 -->
|
||||
<div class="stats-card">
|
||||
<h3>대리점 현황</h3>
|
||||
<div class="stats-number"><?php echo count($dealer_performance); ?></div>
|
||||
<div class="stats-label">활성 대리점</div>
|
||||
<div class="stats-detail">
|
||||
<?php
|
||||
$total_bids = array_sum(array_column($dealer_performance, 'total_bids'));
|
||||
$selected_bids = array_sum(array_column($dealer_performance, 'selected_bids'));
|
||||
$avg_success_rate = $total_bids > 0 ? round(($selected_bids / $total_bids) * 100, 2) : 0;
|
||||
?>
|
||||
총 입찰: <?php echo number_format($total_bids); ?>건 |
|
||||
평균 성공률: <?php echo $avg_success_rate; ?>%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 전문가 방문 카드 -->
|
||||
<div class="stats-card">
|
||||
<h3>전문가 방문</h3>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($expert_visit_stats['visit_statistics']['total_requests'] ?? 0); ?></div>
|
||||
<div class="stats-label">방문 요청</div>
|
||||
<div class="stats-detail">
|
||||
완료: <?php echo number_format($expert_visit_stats['visit_statistics']['completed_visits'] ?? 0); ?>건 |
|
||||
매출: <?php echo number_format($expert_visit_stats['visit_statistics']['total_visit_revenue'] ?? 0); ?>원
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 월별 트렌드 차트 -->
|
||||
<div class="chart-container">
|
||||
<h3>월별 견적 요청 트렌드</h3>
|
||||
<canvas id="monthlyTrendChart" width="800" height="400"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 견적 통계 탭 -->
|
||||
<div id="estimates-tab" class="tab-content">
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>상태별 견적 분포</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">상태</th>
|
||||
<th scope="col">건수</th>
|
||||
<th scope="col">비율</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$total_estimates = $estimate_stats['total_statistics']['total_estimates'];
|
||||
foreach ($estimate_stats['status_distribution'] as $status):
|
||||
$percentage = $total_estimates > 0 ? round(($status['count'] / $total_estimates) * 100, 2) : 0;
|
||||
?>
|
||||
<tr>
|
||||
<td class="td_left"><?php echo get_text($status['status']); ?></td>
|
||||
<td class="td_num"><?php echo number_format($status['count']); ?>건</td>
|
||||
<td class="td_num"><?php echo $percentage; ?>%</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h3>월별 견적 전환율</h3>
|
||||
<canvas id="conversionChart" width="800" height="400"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 대리점 성과 탭 -->
|
||||
<div id="dealers-tab" class="tab-content">
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>대리점별 성과 분석</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">대리점</th>
|
||||
<th scope="col">레벨</th>
|
||||
<th scope="col">총 입찰</th>
|
||||
<th scope="col">선택된 입찰</th>
|
||||
<th scope="col">성공률</th>
|
||||
<th scope="col">평균 입찰금액</th>
|
||||
<th scope="col">총 매출</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($dealer_performance)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="empty_table">대리점 성과 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($dealer_performance as $dealer): ?>
|
||||
<tr>
|
||||
<td class="td_left">
|
||||
<strong><?php echo get_text($dealer['dealer_name']); ?></strong><br>
|
||||
<small><?php echo $dealer['dealer_id']; ?></small>
|
||||
</td>
|
||||
<td class="td_num">레벨 <?php echo $dealer['dealer_level']; ?></td>
|
||||
<td class="td_num"><?php echo number_format($dealer['total_bids']); ?>건</td>
|
||||
<td class="td_num"><?php echo number_format($dealer['selected_bids']); ?>건</td>
|
||||
<td class="td_num">
|
||||
<span
|
||||
class="<?php echo $dealer['success_rate'] >= 30 ? 'text-success' : ($dealer['success_rate'] >= 15 ? 'text-warning' : 'text-danger'); ?>">
|
||||
<?php echo $dealer['success_rate']; ?>%
|
||||
</span>
|
||||
</td>
|
||||
<td class="td_num"><?php echo number_format($dealer['avg_bid_amount']); ?>원</td>
|
||||
<td class="td_num"><?php echo number_format($dealer['total_revenue']); ?>원</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 매출 분석 탭 -->
|
||||
<div id="revenue-tab" class="tab-content">
|
||||
<div class="stats-grid">
|
||||
<div class="stats-card">
|
||||
<h4>총 주문</h4>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($revenue_stats['total_revenue']['total_orders'] ?? 0); ?>건</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h4>총 매출</h4>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($revenue_stats['total_revenue']['total_revenue'] ?? 0); ?>원</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h4>평균 주문금액</h4>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($revenue_stats['total_revenue']['avg_order_value'] ?? 0); ?>원</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h4>결제 완료율</h4>
|
||||
<?php
|
||||
$payment_rate = ($revenue_stats['total_revenue']['total_orders'] ?? 0) > 0
|
||||
? round((($revenue_stats['total_revenue']['paid_orders'] ?? 0) / $revenue_stats['total_revenue']['total_orders']) * 100, 2)
|
||||
: 0;
|
||||
?>
|
||||
<div class="stats-number"><?php echo $payment_rate; ?>%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>결제 단계별 현황</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">결제 단계</th>
|
||||
<th scope="col">건수</th>
|
||||
<th scope="col">매출</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($revenue_stats['payment_breakdown'] as $payment): ?>
|
||||
<tr>
|
||||
<td class="td_left"><?php echo get_text($payment['status']); ?></td>
|
||||
<td class="td_num"><?php echo number_format($payment['count']); ?>건</td>
|
||||
<td class="td_num"><?php echo number_format($payment['revenue']); ?>원</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h3>월별 매출 트렌드</h3>
|
||||
<canvas id="revenueChart" width="800" height="400"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 전문가 방문 탭 -->
|
||||
<div id="expert_visits-tab" class="tab-content">
|
||||
<div class="stats-grid">
|
||||
<div class="stats-card">
|
||||
<h4>총 방문 요청</h4>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($expert_visit_stats['visit_statistics']['total_requests'] ?? 0); ?>건</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h4>완료된 방문</h4>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($expert_visit_stats['visit_statistics']['completed_visits'] ?? 0); ?>건</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h4>방문 매출</h4>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($expert_visit_stats['visit_statistics']['total_visit_revenue'] ?? 0); ?>원</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h4>평균 방문비</h4>
|
||||
<div class="stats-number">
|
||||
<?php echo number_format($expert_visit_stats['visit_statistics']['avg_visit_fee'] ?? 0); ?>원</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>전문가별 방문 성과</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">전문가</th>
|
||||
<th scope="col">배정된 방문</th>
|
||||
<th scope="col">완료된 방문</th>
|
||||
<th scope="col">완료율</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($expert_visit_stats['expert_performance'])): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="empty_table">전문가 방문 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($expert_visit_stats['expert_performance'] as $expert): ?>
|
||||
<tr>
|
||||
<td class="td_left">
|
||||
<strong><?php echo get_text($expert['expert_name']); ?></strong><br>
|
||||
<small><?php echo $expert['expert_id']; ?></small>
|
||||
</td>
|
||||
<td class="td_num"><?php echo number_format($expert['assigned_visits']); ?>건</td>
|
||||
<td class="td_num"><?php echo number_format($expert['completed_visits']); ?>건</td>
|
||||
<td class="td_num">
|
||||
<span
|
||||
class="<?php echo $expert['completion_rate'] >= 80 ? 'text-success' : ($expert['completion_rate'] >= 60 ? 'text-warning' : 'text-danger'); ?>">
|
||||
<?php echo $expert['completion_rate']; ?>%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tab-menu {
|
||||
margin: 20px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
border-bottom-color: #007cba;
|
||||
color: #007cba;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #007cba;
|
||||
}
|
||||
|
||||
.stats-card h3,
|
||||
.stats-card h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #007cba;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stats-detail {
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart-container h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
// 탭 전환
|
||||
function showTab(tabName) {
|
||||
// 모든 탭 비활성화
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// 선택된 탭 활성화
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
}
|
||||
|
||||
// CSV 내보내기
|
||||
function exportCSV() {
|
||||
const exportType = document.getElementById('export_type').value;
|
||||
const dateFrom = document.getElementById('date_from').value;
|
||||
const dateTo = document.getElementById('date_to').value;
|
||||
|
||||
const url = `?export=csv&export_type=${exportType}&date_from=${dateFrom}&date_to=${dateTo}`;
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// 차트 데이터 준비
|
||||
const monthlyTrendData = <?php echo json_encode($estimate_stats['monthly_trend']); ?>;
|
||||
const monthlyRevenueData = <?php echo json_encode($revenue_stats['monthly_revenue']); ?>;
|
||||
|
||||
// 월별 트렌드 차트
|
||||
if (document.getElementById('monthlyTrendChart')) {
|
||||
const ctx1 = document.getElementById('monthlyTrendChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: monthlyTrendData.map(item => item.month),
|
||||
datasets: [{
|
||||
label: '총 견적수',
|
||||
data: monthlyTrendData.map(item => item.total_count),
|
||||
borderColor: '#007cba',
|
||||
backgroundColor: 'rgba(0, 124, 186, 0.1)',
|
||||
tension: 0.4
|
||||
}, {
|
||||
label: '선택된 견적수',
|
||||
data: monthlyTrendData.map(item => item.selected_count),
|
||||
borderColor: '#28a745',
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 전환율 차트
|
||||
if (document.getElementById('conversionChart')) {
|
||||
const ctx2 = document.getElementById('conversionChart').getContext('2d');
|
||||
new Chart(ctx2, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: monthlyTrendData.map(item => item.month),
|
||||
datasets: [{
|
||||
label: '전환율 (%)',
|
||||
data: monthlyTrendData.map(item => item.conversion_rate),
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.8)',
|
||||
borderColor: '#ffc107',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 매출 차트
|
||||
if (document.getElementById('revenueChart')) {
|
||||
const ctx3 = document.getElementById('revenueChart').getContext('2d');
|
||||
new Chart(ctx3, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: monthlyRevenueData.map(item => item.month),
|
||||
datasets: [{
|
||||
label: '월별 매출 (원)',
|
||||
data: monthlyRevenueData.map(item => item.revenue),
|
||||
borderColor: '#dc3545',
|
||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||
tension: 0.4,
|
||||
yAxisID: 'y'
|
||||
}, {
|
||||
label: '주문 건수',
|
||||
data: monthlyRevenueData.map(item => item.order_count),
|
||||
borderColor: '#6f42c1',
|
||||
backgroundColor: 'rgba(111, 66, 193, 0.1)',
|
||||
tension: 0.4,
|
||||
yAxisID: 'y1'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
beginAtZero: true
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
$sub_menu = '800700';
|
||||
include_once('./_common.php');
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
// 상태명 저장 처리
|
||||
if ($_POST['mode'] == 'update_status_names') {
|
||||
foreach ($_POST['status'] as $code => $name) {
|
||||
$code = sql_real_escape_string($code);
|
||||
$name = sql_real_escape_string($name);
|
||||
$config_key = 'status_' . $code;
|
||||
|
||||
sql_query("UPDATE order_config SET config_value = '{$name}' WHERE config_key = '{$config_key}'");
|
||||
}
|
||||
|
||||
alert('상태명이 저장되었습니다.', './status_manager.php');
|
||||
}
|
||||
|
||||
// 현재 상태 설정 조회
|
||||
$status_configs = [];
|
||||
$result = sql_query("SELECT config_key, config_value, config_desc FROM order_config WHERE config_key LIKE 'status_%' ORDER BY config_key");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$status_code = str_replace('status_', '', $row['config_key']);
|
||||
$status_configs[$status_code] = $row;
|
||||
}
|
||||
|
||||
|
||||
$customer_statuses = ['견적신청중', '작성완료', '계약금입금예정', '계약금입금완료', '중도금입금예정', '중도금입금완료', '잔금입금예정', '잔금처리완료', '시공완료'];
|
||||
$agent_statuses = ['견적제안', '견적채택', '견적취소', '시공완료'];
|
||||
|
||||
$g5['title'] = '상태 관리';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>견적 상태 코드는 변경하지 않고, 화면에 표시되는 이름만 수정할 수 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<form name="fstatusform" method="post" action="./status_manager.php">
|
||||
<input type="hidden" name="mode" value="update_status_names">
|
||||
|
||||
<section id="anc_status_manager">
|
||||
<h2>고객 상태 관리</h2>
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>고객 견적 상태 표시명 설정</caption>
|
||||
<colgroup>
|
||||
<col class="grid_3">
|
||||
<col class="grid_3">
|
||||
<col>
|
||||
<col class="grid_2">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">상태 코드</th>
|
||||
<th scope="col">현재 표시명</th>
|
||||
<th scope="col">새 표시명</th>
|
||||
<th scope="col">설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// $customer_statuses = ['견적신청중', '작성완료', '계약금입금예정', '계약금입금완료', '중도금입금예정', '중도금입금완료', '잔금입금예정', '잔금처리완료', '시공완료'];
|
||||
foreach ($customer_statuses as $code) {
|
||||
if (isset($status_configs[$code])) {
|
||||
$config = $status_configs[$code];
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?php echo $code; ?></strong></td>
|
||||
<td><?php echo htmlspecialchars($config['config_value']); ?></td>
|
||||
<td>
|
||||
<input type="text" name="status[<?php echo $code; ?>]"
|
||||
value="<?php echo htmlspecialchars($config['config_value']); ?>" class="frm_input"
|
||||
size="20">
|
||||
</td>
|
||||
<td><?php echo $config['config_desc']; ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top:30px;">대리점 상태 관리</h2>
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>대리점 견적 상태 표시명 설정</caption>
|
||||
<colgroup>
|
||||
<col class="grid_3">
|
||||
<col class="grid_3">
|
||||
<col>
|
||||
<col class="grid_2">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">상태 코드</th>
|
||||
<th scope="col">현재 표시명</th>
|
||||
<th scope="col">새 표시명</th>
|
||||
<th scope="col">설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// $agent_statuses = ['견적제안', '견적채택', '견적취소', '시공완료'];
|
||||
foreach ($agent_statuses as $code) {
|
||||
if (isset($status_configs[$code])) {
|
||||
$config = $status_configs[$code];
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?php echo $code; ?></strong></td>
|
||||
<td><?php echo htmlspecialchars($config['config_value']); ?></td>
|
||||
<td>
|
||||
<input type="text" name="status[<?php echo $code; ?>]"
|
||||
value="<?php echo htmlspecialchars($config['config_value']); ?>" class="frm_input"
|
||||
size="20">
|
||||
</td>
|
||||
<td><?php echo $config['config_desc']; ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top:30px;">상태 전환 규칙</h2>
|
||||
<div class="local_desc02">
|
||||
<h3>고객 상태 전환:</h3>
|
||||
<p><strong>견적신청중</strong> → 작성완료 → 견적채택 → 입금예정 → 입금확인 → 다운로드</p>
|
||||
|
||||
<h3>대리점 상태 전환:</h3>
|
||||
<p><strong>견적제안</strong> → 견적채택 또는 견적취소</p>
|
||||
|
||||
<h3>관리자 권한:</h3>
|
||||
<p>- 모든 상태 변경 가능<br>
|
||||
- 작성완료 → 견적신청중 (되돌리기)<br>
|
||||
- 견적취소 → 견적제안 (되돌리기)<br>
|
||||
- 다운로드 → 견적신청중 (루프백)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="상태명 저장" class="btn_submit">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.local_desc02 {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.local_desc02 h3 {
|
||||
color: #495057;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.local_desc02 p {
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
$sub_menu = "800700";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$g5['title'] = 'StatusManager 테스트';
|
||||
|
||||
// StatusManager 클래스 로드
|
||||
require_once G5_PATH . '/adm/order_manage/classes/StatusManager.class.php';
|
||||
|
||||
$status_manager = new StatusManager();
|
||||
$test_results = [];
|
||||
|
||||
// 테스트 실행
|
||||
if ($_POST['action'] === 'test') {
|
||||
$wr_id = (int) $_POST['wr_id'];
|
||||
$new_status = trim($_POST['new_status']);
|
||||
$user_role = trim($_POST['user_role']);
|
||||
$user_id = $member['mb_id'];
|
||||
|
||||
if ($wr_id && $new_status && $user_role) {
|
||||
$result = $status_manager->changeStatus($wr_id, $new_status, $user_id, $user_role, 'StatusManager 테스트');
|
||||
$test_results[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 견적 목록 조회 (테스트용)
|
||||
$recent_estimates = [];
|
||||
$sql = "
|
||||
SELECT w.wr_id, w.wr_subject, w.mb_id, e.status, e.updated_at
|
||||
FROM {$g5['write_prefix']}order w
|
||||
LEFT JOIN estimate e ON w.wr_id = e.wr_id
|
||||
WHERE w.wr_parent = 0
|
||||
ORDER BY w.wr_datetime DESC
|
||||
LIMIT 10
|
||||
";
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$recent_estimates[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
StatusManager 클래스의 상태 변경 기능을 테스트합니다.<br>
|
||||
실제 데이터가 변경되므로 주의해서 사용하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($test_results)): ?>
|
||||
<div class="local_desc02 local_desc">
|
||||
<h3>테스트 결과</h3>
|
||||
<?php foreach ($test_results as $result): ?>
|
||||
<div
|
||||
style="padding: 10px; margin: 10px 0; border: 1px solid <?php echo $result['success'] ? '#28a745' : '#dc3545'; ?>; background: <?php echo $result['success'] ? '#d4edda' : '#f8d7da'; ?>;">
|
||||
<strong><?php echo $result['success'] ? '성공' : '실패'; ?>:</strong>
|
||||
<?php echo htmlspecialchars($result['message']); ?>
|
||||
<?php if (isset($result['data'])): ?>
|
||||
<br><small>변경 내용: <?php echo htmlspecialchars(json_encode($result['data'], JSON_UNESCAPED_UNICODE)); ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form name="ftest" method="post" action="./status_test.php">
|
||||
<input type="hidden" name="action" value="test">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>StatusManager 테스트</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="wr_id">게시물 ID</label></th>
|
||||
<td>
|
||||
<select name="wr_id" id="wr_id" class="frm_input">
|
||||
<option value="">선택하세요</option>
|
||||
<?php foreach ($recent_estimates as $estimate): ?>
|
||||
<option value="<?php echo $estimate['wr_id']; ?>">
|
||||
<?php echo $estimate['wr_id']; ?> -
|
||||
<?php echo htmlspecialchars($estimate['wr_subject']); ?>
|
||||
(현재: <?php echo $estimate['status'] ?: '견적신청중'; ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="new_status">새 상태</label></th>
|
||||
<td>
|
||||
<select name="new_status" id="new_status" class="frm_input">
|
||||
<option value="">선택하세요</option>
|
||||
<option value="견적신청중">견적신청중</option>
|
||||
<option value="작성완료">작성완료</option>
|
||||
<option value="견적채택">견적채택</option>
|
||||
<option value="입금예정">입금예정</option>
|
||||
<option value="입금확인">입금확인</option>
|
||||
<option value="다운로드">다운로드</option>
|
||||
<option value="견적제안">견적제안</option>
|
||||
<option value="견적취소">견적취소</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="user_role">사용자 역할</label></th>
|
||||
<td>
|
||||
<select name="user_role" id="user_role" class="frm_input">
|
||||
<option value="">선택하세요</option>
|
||||
<option value="admin">관리자</option>
|
||||
<option value="customer">고객</option>
|
||||
<option value="agent">대리점</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="상태 변경 테스트" class="btn_submit">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<h3>최근 견적 목록</h3>
|
||||
<table class="tbl_head01 tbl_wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>제목</th>
|
||||
<th>작성자</th>
|
||||
<th>현재 상태</th>
|
||||
<th>수정일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent_estimates as $estimate): ?>
|
||||
<tr>
|
||||
<td><?php echo $estimate['wr_id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($estimate['wr_subject']); ?></td>
|
||||
<td><?php echo htmlspecialchars($estimate['mb_id']); ?></td>
|
||||
<td><?php echo $estimate['status'] ?: '견적신청중'; ?></td>
|
||||
<td><?php echo $estimate['updated_at'] ?: '-'; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<h3>StatusManager 주요 기능</h3>
|
||||
<ul>
|
||||
<li><strong>상태 전환 검증:</strong> 사용자 권한별 허용된 상태 변경만 가능</li>
|
||||
<li><strong>비즈니스 규칙 검증:</strong> 24시간 제한, 중복 선택 방지 등</li>
|
||||
<li><strong>자동 이력 기록:</strong> 모든 상태 변경이 estimate_history에 기록</li>
|
||||
<li><strong>후속 처리:</strong> 알림 발송, 자동 상태 전환, 다른 견적 취소 등</li>
|
||||
<li><strong>트랜잭션 처리:</strong> 오류 발생 시 자동 롤백</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
include_once('../../../_common.php');
|
||||
|
||||
// 알림 시스템 로드
|
||||
if (file_exists(G5_LIB_PATH . '/notification_helper.php')) {
|
||||
include_once(G5_LIB_PATH . '/notification_helper.php');
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'message' => 'POST 요청만 허용됩니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$wr_id = (int) ($_POST['wr_id'] ?? 0);
|
||||
$new_status = trim($_POST['new_status'] ?? '');
|
||||
$memo = trim($_POST['memo'] ?? '');
|
||||
|
||||
if (!$wr_id || !$new_status) {
|
||||
echo json_encode(['success' => false, 'message' => '필수 파라미터가 누락되었습니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$is_member) {
|
||||
echo json_encode(['success' => false, 'message' => '로그인이 필요합니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_admin = ($member['mb_level'] ?? 0) >= 8;
|
||||
$is_agent = in_array(($member['mb_level'] ?? 0), [5, 6, 7]);
|
||||
|
||||
try {
|
||||
sql_query("START TRANSACTION");
|
||||
|
||||
$write_table = $g5['write_prefix'] . 'order';
|
||||
$write = sql_fetch("SELECT * FROM {$write_table} WHERE wr_id = '{$wr_id}'");
|
||||
|
||||
if (!$write) {
|
||||
throw new Exception('게시물을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$is_owner = ($member['mb_id'] === $write['mb_id']);
|
||||
|
||||
if (!$is_admin && !$is_owner && !$is_agent) {
|
||||
throw new Exception('권한이 없습니다.');
|
||||
}
|
||||
|
||||
$current_estimate = sql_fetch("SELECT * FROM estimate WHERE wr_id = '{$wr_id}'");
|
||||
$old_status = $current_estimate ? $current_estimate['status'] : '견적신청중';
|
||||
|
||||
// 권한별 상태 변경 검증
|
||||
$allowed = false;
|
||||
if ($is_admin) {
|
||||
$allowed = true; // 관리자는 모든 상태 변경 가능
|
||||
} elseif ($is_owner && empty($write['wr_parent'])) {
|
||||
// 고객 (원본글 작성자)
|
||||
if ($old_status === '견적신청중' && $new_status === '작성완료') {
|
||||
$allowed = true;
|
||||
} elseif ($old_status === '작성완료' && $new_status === '견적채택') {
|
||||
$allowed = true;
|
||||
}
|
||||
} elseif ($is_agent && !empty($write['wr_parent']) && $write['mb_id'] === $member['mb_id']) {
|
||||
// 대리점 (답글 작성자)
|
||||
if ($old_status === '견적제안' && in_array($new_status, ['견적채택', '견적취소'])) {
|
||||
$allowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allowed) {
|
||||
throw new Exception('해당 상태로 변경할 권한이 없습니다.');
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
if ($current_estimate) {
|
||||
sql_query("UPDATE estimate SET
|
||||
status = '{$new_status}',
|
||||
updated_at = NOW(),
|
||||
updated_by = '{$member['mb_id']}'
|
||||
WHERE wr_id = '{$wr_id}'");
|
||||
} else {
|
||||
sql_query("INSERT INTO estimate (wr_id, status, created_at, created_by, updated_at, updated_by)
|
||||
VALUES ('{$wr_id}', '{$new_status}', NOW(), '{$member['mb_id']}', NOW(), '{$member['mb_id']}')");
|
||||
}
|
||||
|
||||
// 게시판 wr_1 필드도 업데이트
|
||||
sql_query("UPDATE {$write_table} SET wr_1 = '{$new_status}' WHERE wr_id = '{$wr_id}'");
|
||||
|
||||
// 이력 기록
|
||||
$history_data = json_encode([
|
||||
'old_status' => $old_status,
|
||||
'new_status' => $new_status,
|
||||
'changed_by' => $member['mb_id'],
|
||||
'changed_at' => date('Y-m-d H:i:s'),
|
||||
'memo' => $memo,
|
||||
'ip' => $_SERVER['REMOTE_ADDR']
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$estimate = sql_fetch("SELECT id FROM estimate WHERE wr_id = '{$wr_id}'");
|
||||
$estimate_id = $estimate ? $estimate['id'] : 0;
|
||||
|
||||
sql_query("INSERT INTO estimate_history (
|
||||
estimate_id, action, change_details, changed_by, changed_at
|
||||
) VALUES (
|
||||
'{$estimate_id}', 'status_change', '{$history_data}', '{$member['mb_id']}', NOW()
|
||||
)");
|
||||
|
||||
// 알림 발송
|
||||
processStatusChangeNotification($write, $old_status, $new_status, $member);
|
||||
|
||||
// 견적채택 특별 처리
|
||||
if ($new_status === '견적채택' && !empty($write['wr_parent'])) {
|
||||
handleQuoteSelection($write, $member);
|
||||
}
|
||||
|
||||
sql_query("COMMIT");
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => '상태가 성공적으로 변경되었습니다.',
|
||||
'data' => [
|
||||
'old_status' => $old_status,
|
||||
'new_status' => $new_status
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
sql_query("ROLLBACK");
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
function processStatusChangeNotification($write, $old_status, $new_status, $member) {
|
||||
switch ($new_status) {
|
||||
case '작성완료':
|
||||
notifyAgentsNewRequest($write);
|
||||
break;
|
||||
case '견적제안':
|
||||
notifyCustomerQuoteReceived($write);
|
||||
break;
|
||||
case '입금확인':
|
||||
notifyPaymentConfirmed($write, $member);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function notifyAgentsNewRequest($write) {
|
||||
$agents_sql = "SELECT mb_id, mb_name, mb_email, mb_hp FROM {$GLOBALS['g5']['member_table']}
|
||||
WHERE mb_level IN (5,6,7) AND mb_leave_date = '' AND mb_intercept_date = ''";
|
||||
$agents = sql_query($agents_sql);
|
||||
|
||||
while ($agent = sql_fetch_array($agents)) {
|
||||
$subject = "[견적요청] 새로운 견적 요청이 등록되었습니다";
|
||||
$content = "안녕하세요 {$agent['mb_name']}님,\n\n새로운 견적 요청이 등록되었습니다.\n\n고객명: {$write['wr_name']}\n요청제목: {$write['wr_subject']}\n등록시간: " . date('Y-m-d H:i') . "\n\n확인 URL: " . G5_HTTP_BBS_URL . "/board.php?bo_table=order&wr_id={$write['wr_id']}\n\n감사합니다.";
|
||||
|
||||
@mailer($agent['mb_name'], $agent['mb_email'], $subject, $content, 1);
|
||||
|
||||
if ($agent['mb_hp']) {
|
||||
@send_sms($agent['mb_hp'], "[견적요청] {$write['wr_name']}님의 새 견적요청이 등록되었습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifyCustomerQuoteReceived($write) {
|
||||
$parent_write = sql_fetch("SELECT * FROM {$GLOBALS['g5']['write_prefix']}order WHERE wr_id = '{$write['wr_parent']}'");
|
||||
|
||||
if ($parent_write) {
|
||||
$customer = get_member($parent_write['mb_id']);
|
||||
$agent_name = get_member_name($write['mb_id']);
|
||||
|
||||
@mailer($parent_write['wr_name'], $customer['mb_email'], "[견적도착] {$agent_name}님이 견적을 제안했습니다",
|
||||
"안녕하세요 {$parent_write['wr_name']}님,\n\n{$agent_name}님이 견적을 제안했습니다.\n\n확인해주세요.", 1);
|
||||
|
||||
if ($customer['mb_hp']) {
|
||||
@send_sms($customer['mb_hp'], "[견적도착] {$agent_name}님이 견적을 제안했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifyPaymentConfirmed($write, $admin_member) {
|
||||
$customer = get_member($write['mb_id']);
|
||||
@mailer($write['wr_name'], $customer['mb_email'], "[입금확인] 입금이 확인되었습니다",
|
||||
"안녕하세요 {$write['wr_name']}님,\n\n입금이 확인되었습니다.\n\n감사합니다.", 1);
|
||||
|
||||
if ($customer['mb_hp']) {
|
||||
@send_sms($customer['mb_hp'], "[입금확인] 입금이 확인되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuoteSelection($write, $member) {
|
||||
$origin_wr_id = $write['wr_parent'];
|
||||
$write_table = $GLOBALS['g5']['write_prefix'] . 'order';
|
||||
|
||||
sql_query("UPDATE estimate SET status = '입금예정' WHERE wr_id = '{$origin_wr_id}'");
|
||||
sql_query("UPDATE {$write_table} SET wr_1 = '견적취소' WHERE wr_parent = '{$origin_wr_id}' AND wr_id != '{$write['wr_id']}'");
|
||||
|
||||
$agent = get_member($write['mb_id']);
|
||||
@mailer($agent['mb_name'], $agent['mb_email'], "[견적채택] 축하합니다!", "견적이 채택되었습니다.", 1);
|
||||
|
||||
if ($agent['mb_hp']) {
|
||||
@send_sms($agent['mb_hp'], "[견적채택] 축하합니다! 견적이 채택되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
function get_member_name($mb_id) {
|
||||
if (!$mb_id) return '';
|
||||
$member = sql_fetch("SELECT mb_name FROM {$GLOBALS['g5']['member_table']} WHERE mb_id = '{$mb_id}'");
|
||||
return $member ? $member['mb_name'] : $mb_id;
|
||||
}
|
||||
|
||||
function send_sms($phone, $message) {
|
||||
return true;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
require_once './_common.php';
|
||||
|
||||
// 관리자 권한 체크
|
||||
if (!$is_admin) {
|
||||
die('관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
echo "<h2>견적서 시스템 기능 테스트</h2>";
|
||||
|
||||
// 1. 데이터베이스 테이블 확인
|
||||
echo "<h3>1. 데이터베이스 테이블 확인</h3>";
|
||||
$tables_to_check = ['order_config', 'order_mail_templates', 'order_sms_templates', 'estimate'];
|
||||
|
||||
foreach ($tables_to_check as $table) {
|
||||
$full_table_name = ($table === 'estimate') ? $table : G5_TABLE_PREFIX . $table;
|
||||
$result = sql_query("SHOW TABLES LIKE '{$full_table_name}'", false);
|
||||
if ($result && sql_num_rows($result) > 0) {
|
||||
echo "<p style='color: green;'>✓ {$table} 테이블 존재</p>";
|
||||
|
||||
// 데이터 개수 확인
|
||||
$count_result = sql_query("SELECT COUNT(*) as cnt FROM {$full_table_name}", false);
|
||||
if ($count_result) {
|
||||
$count_row = sql_fetch_array($count_result);
|
||||
echo "<p style='margin-left: 20px;'>- 데이터 개수: {$count_row['cnt']}개</p>";
|
||||
}
|
||||
} else {
|
||||
echo "<p style='color: red;'>✗ {$table} 테이블 없음</p>";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. notification_helper.php 함수 테스트
|
||||
echo "<h3>2. 알림 시스템 함수 테스트</h3>";
|
||||
include_once G5_LIB_PATH . '/notification_helper.php';
|
||||
|
||||
// get_order_config 함수 테스트
|
||||
$timer_enabled = get_order_config('timer_enabled', '1');
|
||||
$visit_fee = get_order_config('expert_visit_fee', '50000');
|
||||
echo "<p>타이머 활성화: " . ($timer_enabled ? '예' : '아니오') . "</p>";
|
||||
echo "<p>전문가 방문 비용: " . number_format($visit_fee) . "원</p>";
|
||||
|
||||
// 3. 템플릿 시스템 테스트
|
||||
echo "<h3>3. 템플릿 시스템 테스트</h3>";
|
||||
$mail_templates = sql_query("SELECT template_key, template_name FROM order_mail_templates LIMIT 5");
|
||||
if ($mail_templates && sql_num_rows($mail_templates) > 0) {
|
||||
echo "<p style='color: green;'>✓ 메일 템플릿 로드 성공</p>";
|
||||
echo "<ul>";
|
||||
while ($row = sql_fetch_array($mail_templates)) {
|
||||
echo "<li>{$row['template_key']}: {$row['template_name']}</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
} else {
|
||||
echo "<p style='color: red;'>✗ 메일 템플릿 없음</p>";
|
||||
}
|
||||
|
||||
$sms_templates = sql_query("SELECT template_key, template_name FROM order_sms_templates LIMIT 5");
|
||||
if ($sms_templates && sql_num_rows($sms_templates) > 0) {
|
||||
echo "<p style='color: green;'>✓ SMS 템플릿 로드 성공</p>";
|
||||
echo "<ul>";
|
||||
while ($row = sql_fetch_array($sms_templates)) {
|
||||
echo "<li>{$row['template_key']}: {$row['template_name']}</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
} else {
|
||||
echo "<p style='color: red;'>✗ SMS 템플릿 없음</p>";
|
||||
}
|
||||
|
||||
// 4. estimate 테이블 구조 확인
|
||||
echo "<h3>4. estimate 테이블 구조 확인</h3>";
|
||||
$result = sql_query("DESCRIBE estimate", false);
|
||||
if ($result) {
|
||||
echo "<table border='1' style='border-collapse: collapse;'>";
|
||||
echo "<tr><th>컬럼명</th><th>타입</th><th>설명</th></tr>";
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$description = '';
|
||||
if (strpos($row['Field'], 'temp_') === 0) {
|
||||
switch ($row['Field']) {
|
||||
case 'temp_1':
|
||||
$description = '전문가 방문 요청 여부 (Y/N)';
|
||||
break;
|
||||
case 'temp_2':
|
||||
$description = '전문가 방문 상태';
|
||||
break;
|
||||
case 'temp_3':
|
||||
$description = '전문가 방문 비용';
|
||||
break;
|
||||
case 'temp_4':
|
||||
$description = '전문가 방문 일정';
|
||||
break;
|
||||
case 'temp_5':
|
||||
$description = '전문가 방문 메모';
|
||||
break;
|
||||
}
|
||||
} elseif (strpos($row['Field'], 'extra_') === 0) {
|
||||
switch ($row['Field']) {
|
||||
case 'extra_1':
|
||||
$description = '고객 연락처';
|
||||
break;
|
||||
case 'extra_2':
|
||||
$description = '결제 상태';
|
||||
break;
|
||||
case 'extra_3':
|
||||
$description = '추가 요청사항';
|
||||
break;
|
||||
case 'extra_4':
|
||||
$description = '관리자 메모';
|
||||
break;
|
||||
case 'extra_5':
|
||||
$description = '예비 필드';
|
||||
break;
|
||||
}
|
||||
}
|
||||
echo "<tr><td>{$row['Field']}</td><td>{$row['Type']}</td><td>{$description}</td></tr>";
|
||||
}
|
||||
echo "</table>";
|
||||
} else {
|
||||
echo "<p style='color: red;'>✗ estimate 테이블 구조 확인 실패</p>";
|
||||
}
|
||||
|
||||
// 5. 24시간 타이머 계산 테스트
|
||||
echo "<h3>5. 24시간 타이머 계산 테스트</h3>";
|
||||
$now = time();
|
||||
$test_time = date('Y-m-d H:i:s', $now - 3600); // 1시간 전
|
||||
$test_timestamp = strtotime($test_time);
|
||||
$elapsed = $now - $test_timestamp;
|
||||
$limit_seconds = 24 * 60 * 60; // 24시간
|
||||
$remain = $limit_seconds - $elapsed;
|
||||
|
||||
echo "<p>현재 시간: " . date('Y-m-d H:i:s', $now) . "</p>";
|
||||
echo "<p>테스트 시작 시간: {$test_time}</p>";
|
||||
echo "<p>경과 시간: " . gmdate('H:i:s', $elapsed) . "</p>";
|
||||
echo "<p>남은 시간: " . gmdate('H:i:s', $remain) . "</p>";
|
||||
echo "<p>타이머 활성화: " . ($remain > 0 ? '예' : '아니오') . "</p>";
|
||||
|
||||
echo "<hr>";
|
||||
echo "<h3>테스트 완료</h3>";
|
||||
echo "<p><a href='./execute_update.php'>데이터베이스 업데이트 실행</a></p>";
|
||||
echo "<p><a href='./config_manager.php'>시스템 설정 관리</a></p>";
|
||||
echo "<p><a href='./expert_visits.php'>전문가 방문 관리</a></p>";
|
||||
echo "<p><a href='./mail_templates.php'>메일 템플릿 관리</a></p>";
|
||||
echo "<p><a href='./sms_templates.php'>SMS 템플릿 관리</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,120 @@
|
||||
-- 기존 테이블 활용 최소 변경 SQL
|
||||
-- 기존 temp_, extra_ 컬럼을 활용하여 새 기능 구현
|
||||
|
||||
-- 1. 시스템 설정을 위한 간단한 테이블 (기존 테이블 없을 경우만 생성)
|
||||
CREATE TABLE IF NOT EXISTS `order_config` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`config_key` varchar(100) NOT NULL,
|
||||
`config_value` text NOT NULL,
|
||||
`config_desc` varchar(255) DEFAULT NULL,
|
||||
`config_type` varchar(20) DEFAULT 'text',
|
||||
`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=utf8;
|
||||
|
||||
-- 기본 설정값 삽입 (중복 시 무시)
|
||||
INSERT IGNORE INTO `order_config` (`config_key`, `config_value`, `config_desc`, `config_type`) VALUES
|
||||
('timer_enabled', '1', '24시간 타이머 활성화 여부', 'boolean'),
|
||||
('timer_message_active', '견적 제안 마감까지 {time} 남았습니다.', '타이머 활성화 시 메시지', 'text'),
|
||||
('timer_message_inactive', '고객 작성이 완료되었습니다. 24시간 후에 확인 바랍니다.', '타이머 비활성화 시 메시지', 'text'),
|
||||
('contract_deposit_rate', '10', '계약금 비율 (%)', 'number'),
|
||||
('middle_payment_rate', '40', '중도금 비율 (%)', 'number'),
|
||||
('expert_visit_fee', '50000', '전문가 방문 비용', 'number'),
|
||||
('expert_account_info', '국민은행 123-456-789 (주)창호전문가', '전문가 방문 계좌 정보', 'text');
|
||||
|
||||
-- 2. 메일 템플릿 테이블 (기존 테이블 없을 경우만 생성)
|
||||
CREATE TABLE IF NOT EXISTS `order_mail_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_name` varchar(255) NOT NULL,
|
||||
`subject` varchar(255) NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`variables` text DEFAULT NULL COMMENT '사용 가능한 변수들 (JSON)',
|
||||
`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=utf8;
|
||||
|
||||
-- 3. SMS 템플릿 테이블 (기존 테이블 없을 경우만 생성)
|
||||
CREATE TABLE IF NOT EXISTS `order_sms_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_name` varchar(255) NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`variables` text DEFAULT NULL COMMENT '사용 가능한 변수들 (JSON)',
|
||||
`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=utf8;
|
||||
|
||||
-- 4. 기존 estimate 테이블의 temp_ 컬럼 활용 정의
|
||||
-- temp_1: 전문가 방문 요청 여부 ('Y'/'N')
|
||||
-- temp_2: 전문가 방문 상태 ('requested'/'scheduled'/'completed'/'cancelled')
|
||||
-- temp_3: 전문가 방문 비용
|
||||
-- temp_4: 전문가 방문 일정 (YYYY-MM-DD HH:MM)
|
||||
-- temp_5: 전문가 방문 메모
|
||||
|
||||
-- 5. 기본 메일 템플릿 삽입 (중복 시 무시)
|
||||
INSERT IGNORE INTO `order_mail_templates` (`template_key`, `template_name`, `subject`, `content`, `variables`) VALUES
|
||||
('customer_request_complete', '고객 - 견적 요청 완료', '견적 요청이 완료되었습니다',
|
||||
'안녕하세요 {customer_name}님,<br><br>견적 요청이 성공적으로 완료되었습니다.<br>24시간 후에 견적서를 확인해주시기 바랍니다.<br><br>감사합니다.',
|
||||
'["customer_name", "request_title", "request_date"]'),
|
||||
|
||||
('agent_new_request', '대리점 - 새 견적 요청', '새로운 견적 요청이 등록되었습니다',
|
||||
'안녕하세요 {agent_name}님,<br><br>{customer_name}님이 견적을 요청했습니다.<br><br>제목: {request_title}<br>요청일: {request_date}<br><br>견적 작성 URL: {write_url}<br><br>24시간 내에 견적을 제안해주시기 바랍니다.',
|
||||
'["agent_name", "customer_name", "request_title", "request_date", "write_url"]'),
|
||||
|
||||
('customer_quote_selected', '고객 - 견적 선택 완료', '견적이 선택되었습니다',
|
||||
'안녕하세요 {customer_name}님,<br><br>{agent_name} 대리점의 견적이 선택되었습니다.<br><br>계약금({deposit_amount}원)을 입금해주시기 바랍니다.<br><br>계좌정보: {account_info}',
|
||||
'["customer_name", "agent_name", "deposit_amount", "account_info"]'),
|
||||
|
||||
('agent_quote_selected', '대리점 - 견적 선택됨', '견적이 선택되었습니다',
|
||||
'안녕하세요 {agent_name}님,<br><br>{customer_name}님이 귀하의 견적을 선택하셨습니다.<br><br>시공 일정을 관리자에게 알려주시기 바랍니다.<br><br>감사합니다.',
|
||||
'["agent_name", "customer_name", "request_title"]'),
|
||||
|
||||
('expert_visit_request', '전문가 방문 요청', '전문가 방문이 요청되었습니다',
|
||||
'안녕하세요 {customer_name}님,<br><br>전문가 방문이 요청되었습니다.<br><br>방문 비용: {visit_fee}원<br>계좌정보: {account_info}<br><br>입금 확인 후 방문 일정을 조율하겠습니다.',
|
||||
'["customer_name", "visit_fee", "account_info"]');
|
||||
|
||||
-- 6. 기본 SMS 템플릿 삽입 (중복 시 무시)
|
||||
INSERT IGNORE INTO `order_sms_templates` (`template_key`, `template_name`, `content`, `variables`) VALUES
|
||||
('customer_request_complete', '고객 - 견적 요청 완료',
|
||||
'{customer_name}님, 견적 요청이 완료되었습니다. 24시간 후에 확인해주세요.',
|
||||
'["customer_name"]'),
|
||||
|
||||
('agent_new_request', '대리점 - 새 견적 요청',
|
||||
'{agent_name}님, {customer_name}님이 견적을 요청했습니다. 24시간 내에 제안해주세요. {write_url}',
|
||||
'["agent_name", "customer_name", "write_url"]'),
|
||||
|
||||
('customer_quote_selected', '고객 - 견적 선택 완료',
|
||||
'{customer_name}님, 견적이 선택되었습니다. 계약금 {deposit_amount}원을 입금해주세요.',
|
||||
'["customer_name", "deposit_amount"]'),
|
||||
|
||||
('agent_quote_selected', '대리점 - 견적 선택됨',
|
||||
'{agent_name}님, {customer_name}님이 귀하의 견적을 선택하셨습니다. 시공 일정을 알려주세요.',
|
||||
'["agent_name", "customer_name"]'),
|
||||
|
||||
('expert_visit_request', '전문가 방문 요청',
|
||||
'{customer_name}님, 전문가 방문이 요청되었습니다. 방문비 {visit_fee}원 입금 후 일정 조율하겠습니다.',
|
||||
'["customer_name", "visit_fee"]');
|
||||
|
||||
-- 7. 기존 테이블 컬럼 활용 방법 주석
|
||||
/*
|
||||
기존 estimate 테이블의 temp_ 컬럼 활용:
|
||||
- temp_1: 전문가 방문 요청 여부 ('Y'/'N')
|
||||
- temp_2: 전문가 방문 상태 ('requested'/'scheduled'/'completed'/'cancelled')
|
||||
- temp_3: 전문가 방문 비용 (숫자)
|
||||
- temp_4: 전문가 방문 일정 (YYYY-MM-DD HH:MM 형식)
|
||||
- temp_5: 전문가 방문 메모/특이사항
|
||||
|
||||
기존 estimate 테이블의 extra_ 컬럼 활용:
|
||||
- extra_1: 고객 연락처 (전문가 방문용)
|
||||
- extra_2: 결제 상태 ('pending'/'paid'/'cancelled')
|
||||
- extra_3: 추가 요청사항
|
||||
- extra_4: 관리자 메모
|
||||
- extra_5: 예비 필드
|
||||
*/
|
||||
@@ -0,0 +1,670 @@
|
||||
<?php
|
||||
$sub_menu = "800760";
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check($auth[$sub_menu], 'r');
|
||||
|
||||
$g5['title'] = '워크플로우 통합 테스트';
|
||||
|
||||
// 필요한 클래스들 로드
|
||||
require_once G5_PATH . '/adm/order_manage/classes/EstimateManager.class.php';
|
||||
require_once G5_PATH . '/adm/order_manage/classes/StatusManager.class.php';
|
||||
require_once G5_PATH . '/adm/order_manage/classes/NotificationHelper.class.php';
|
||||
|
||||
$estimate_manager = new EstimateManager();
|
||||
$status_manager = new StatusManager();
|
||||
$notification_helper = new NotificationHelper();
|
||||
|
||||
// 테스트 액션 처리
|
||||
$action = isset($_REQUEST['action']) ? clean_xss_tags($_REQUEST['action']) : '';
|
||||
$test_results = [];
|
||||
|
||||
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check($auth[$sub_menu], 'w');
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'test_customer_workflow':
|
||||
$test_results = runCustomerWorkflowTest();
|
||||
break;
|
||||
case 'test_dealer_workflow':
|
||||
$test_results = runDealerWorkflowTest();
|
||||
break;
|
||||
case 'test_admin_workflow':
|
||||
$test_results = runAdminWorkflowTest();
|
||||
break;
|
||||
case 'test_expert_visit_workflow':
|
||||
$test_results = runExpertVisitWorkflowTest();
|
||||
break;
|
||||
case 'test_full_integration':
|
||||
$test_results = runFullIntegrationTest();
|
||||
break;
|
||||
case 'cleanup_test_data':
|
||||
$test_results = cleanupTestData();
|
||||
break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$test_results = [
|
||||
'success' => false,
|
||||
'message' => '테스트 실행 중 오류 발생: ' . $e->getMessage(),
|
||||
'details' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 테스트 함수들
|
||||
function runCustomerWorkflowTest()
|
||||
{
|
||||
global $estimate_manager, $status_manager;
|
||||
|
||||
$results = [
|
||||
'success' => true,
|
||||
'message' => '고객 워크플로우 테스트',
|
||||
'details' => []
|
||||
];
|
||||
|
||||
try {
|
||||
// 1. 테스트 견적 생성
|
||||
$test_data = [
|
||||
'wr_id' => 99999, // 테스트용 ID
|
||||
'zip_code' => '12345',
|
||||
'address1' => '서울시 강남구',
|
||||
'address2' => '테스트동 123-45',
|
||||
'house_type' => '아파트',
|
||||
'house_size' => '84㎡',
|
||||
'material' => '시스템창호',
|
||||
'color' => '화이트',
|
||||
'glass_thickness' => '24mm',
|
||||
'install' => '전체교체',
|
||||
'items' => [
|
||||
[
|
||||
'product' => '거실 창호',
|
||||
'qty' => 2,
|
||||
'price' => 500000,
|
||||
'amount' => 1000000
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
// 견적 생성 테스트
|
||||
$estimate_id = $estimate_manager->createEstimate($test_data);
|
||||
if ($estimate_id) {
|
||||
$results['details'][] = ['step' => '견적 생성', 'status' => 'PASS', 'message' => "견적 ID: {$estimate_id}"];
|
||||
} else {
|
||||
throw new Exception('견적 생성 실패');
|
||||
}
|
||||
|
||||
// 2. 상태 변경 테스트 (견적신청중 → 작성완료)
|
||||
$status_result = $estimate_manager->completeEstimate(99999, 'customer');
|
||||
if ($status_result['success']) {
|
||||
$results['details'][] = ['step' => '견적 완료', 'status' => 'PASS', 'message' => $status_result['message']];
|
||||
} else {
|
||||
throw new Exception('견적 완료 실패: ' . $status_result['message']);
|
||||
}
|
||||
|
||||
// 3. 견적 선택 시뮬레이션 (가상의 입찰 생성 후 선택)
|
||||
$bid_data = [
|
||||
'total_amount' => 1200000,
|
||||
'message' => '테스트 입찰입니다.'
|
||||
];
|
||||
|
||||
$bid_result = $estimate_manager->createBiddingWithItems($estimate_id, $bid_data, []);
|
||||
if ($bid_result['success']) {
|
||||
$results['details'][] = ['step' => '입찰 생성', 'status' => 'PASS', 'message' => '테스트 입찰 생성됨'];
|
||||
|
||||
// 견적 선택
|
||||
$select_result = $estimate_manager->selectBidWithStatus($estimate_id, $bid_result['data']['bidding_id'], 'customer');
|
||||
if ($select_result['success']) {
|
||||
$results['details'][] = ['step' => '견적 선택', 'status' => 'PASS', 'message' => $select_result['message']];
|
||||
} else {
|
||||
$results['details'][] = ['step' => '견적 선택', 'status' => 'FAIL', 'message' => $select_result['message']];
|
||||
}
|
||||
} else {
|
||||
$results['details'][] = ['step' => '입찰 생성', 'status' => 'FAIL', 'message' => $bid_result['message']];
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$results['success'] = false;
|
||||
$results['details'][] = ['step' => '오류', 'status' => 'FAIL', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function runDealerWorkflowTest()
|
||||
{
|
||||
global $estimate_manager;
|
||||
|
||||
$results = [
|
||||
'success' => true,
|
||||
'message' => '대리점 워크플로우 테스트',
|
||||
'details' => []
|
||||
];
|
||||
|
||||
try {
|
||||
// 1. 기존 견적에 입찰 제출 테스트
|
||||
$estimate = sql_fetch("SELECT * FROM estimate WHERE is_deleted = 0 AND status = '작성완료' LIMIT 1");
|
||||
|
||||
if (!$estimate) {
|
||||
// 테스트용 견적이 없으면 생성
|
||||
$results['details'][] = ['step' => '테스트 견적 확인', 'status' => 'INFO', 'message' => '테스트용 견적이 없어 고객 워크플로우를 먼저 실행하세요.'];
|
||||
return $results;
|
||||
}
|
||||
|
||||
$bid_data = [
|
||||
'total_amount' => 1500000,
|
||||
'message' => '대리점 테스트 입찰입니다.'
|
||||
];
|
||||
|
||||
$items_data = [
|
||||
[
|
||||
'product' => '테스트 창호',
|
||||
'qty' => 1,
|
||||
'price' => 1500000,
|
||||
'amount' => 1500000,
|
||||
'brand' => '테스트브랜드'
|
||||
]
|
||||
];
|
||||
|
||||
$bid_result = $estimate_manager->createBiddingWithItems($estimate['id'], $bid_data, $items_data);
|
||||
if ($bid_result['success']) {
|
||||
$results['details'][] = ['step' => '입찰 제출', 'status' => 'PASS', 'message' => '입찰이 성공적으로 제출됨'];
|
||||
} else {
|
||||
throw new Exception('입찰 제출 실패: ' . $bid_result['message']);
|
||||
}
|
||||
|
||||
// 2. 시공 일정 설정 테스트 (선택된 견적이 있는 경우)
|
||||
if ($estimate['selected_bid_id']) {
|
||||
$construction_date = date('Y-m-d', strtotime('+7 days'));
|
||||
$schedule_result = $estimate_manager->setConstructionSchedule($estimate['wr_id'], $construction_date, 'agent');
|
||||
|
||||
if ($schedule_result['success']) {
|
||||
$results['details'][] = ['step' => '시공 일정 설정', 'status' => 'PASS', 'message' => $schedule_result['message']];
|
||||
} else {
|
||||
$results['details'][] = ['step' => '시공 일정 설정', 'status' => 'FAIL', 'message' => $schedule_result['message']];
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$results['success'] = false;
|
||||
$results['details'][] = ['step' => '오류', 'status' => 'FAIL', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function runAdminWorkflowTest()
|
||||
{
|
||||
global $estimate_manager, $status_manager;
|
||||
|
||||
$results = [
|
||||
'success' => true,
|
||||
'message' => '관리자 워크플로우 테스트',
|
||||
'details' => []
|
||||
];
|
||||
|
||||
try {
|
||||
// 1. 상태 관리 테스트
|
||||
$estimate = sql_fetch("SELECT * FROM estimate WHERE is_deleted = 0 LIMIT 1");
|
||||
|
||||
if ($estimate) {
|
||||
$current_status = $status_manager->getCurrentStatus($estimate['wr_id']);
|
||||
if ($current_status) {
|
||||
$results['details'][] = ['step' => '상태 조회', 'status' => 'PASS', 'message' => "현재 상태: {$current_status['status']}"];
|
||||
} else {
|
||||
$results['details'][] = ['step' => '상태 조회', 'status' => 'FAIL', 'message' => '상태 조회 실패'];
|
||||
}
|
||||
|
||||
// 2. 결제 상태 업데이트 테스트
|
||||
if ($estimate['selected_bid_id']) {
|
||||
$payment_result = $estimate_manager->updatePaymentStatus($estimate['wr_id'], 'deposit', 'admin');
|
||||
if ($payment_result['success']) {
|
||||
$results['details'][] = ['step' => '결제 상태 업데이트', 'status' => 'PASS', 'message' => $payment_result['message']];
|
||||
} else {
|
||||
$results['details'][] = ['step' => '결제 상태 업데이트', 'status' => 'FAIL', 'message' => $payment_result['message']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 통계 데이터 조회 테스트
|
||||
$stats = $estimate_manager->getEstimateStatistics();
|
||||
if (!empty($stats['total_statistics'])) {
|
||||
$results['details'][] = ['step' => '통계 조회', 'status' => 'PASS', 'message' => "총 견적: {$stats['total_statistics']['total_estimates']}건"];
|
||||
} else {
|
||||
$results['details'][] = ['step' => '통계 조회', 'status' => 'FAIL', 'message' => '통계 데이터 조회 실패'];
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$results['success'] = false;
|
||||
$results['details'][] = ['step' => '오류', 'status' => 'FAIL', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function runExpertVisitWorkflowTest()
|
||||
{
|
||||
global $estimate_manager;
|
||||
|
||||
$results = [
|
||||
'success' => true,
|
||||
'message' => '전문가 방문 워크플로우 테스트',
|
||||
'details' => []
|
||||
];
|
||||
|
||||
try {
|
||||
// 1. 전문가 방문 요청 테스트
|
||||
$estimate = sql_fetch("SELECT * FROM estimate WHERE is_deleted = 0 AND temp_1 != 'Y' LIMIT 1");
|
||||
|
||||
if ($estimate) {
|
||||
$visit_result = $estimate_manager->requestExpertVisit($estimate['wr_id'], 'customer');
|
||||
if ($visit_result['success']) {
|
||||
$results['details'][] = ['step' => '전문가 방문 요청', 'status' => 'PASS', 'message' => $visit_result['message']];
|
||||
|
||||
// 2. 결제 확인 테스트
|
||||
$payment_result = $estimate_manager->confirmExpertVisitPayment($estimate['wr_id'], 'admin');
|
||||
if ($payment_result['success']) {
|
||||
$results['details'][] = ['step' => '방문 비용 결제 확인', 'status' => 'PASS', 'message' => $payment_result['message']];
|
||||
|
||||
// 3. 일정 설정 테스트
|
||||
$visit_datetime = date('Y-m-d H:i', strtotime('+3 days 14:00'));
|
||||
$schedule_result = $estimate_manager->scheduleExpertVisit($estimate['wr_id'], $visit_datetime, 'admin', 'admin');
|
||||
if ($schedule_result['success']) {
|
||||
$results['details'][] = ['step' => '방문 일정 설정', 'status' => 'PASS', 'message' => $schedule_result['message']];
|
||||
|
||||
// 4. 방문 완료 테스트
|
||||
$complete_result = $estimate_manager->completeExpertVisit($estimate['wr_id'], '테스트 방문 완료', 'admin');
|
||||
if ($complete_result['success']) {
|
||||
$results['details'][] = ['step' => '방문 완료 처리', 'status' => 'PASS', 'message' => $complete_result['message']];
|
||||
} else {
|
||||
$results['details'][] = ['step' => '방문 완료 처리', 'status' => 'FAIL', 'message' => $complete_result['message']];
|
||||
}
|
||||
} else {
|
||||
$results['details'][] = ['step' => '방문 일정 설정', 'status' => 'FAIL', 'message' => $schedule_result['message']];
|
||||
}
|
||||
} else {
|
||||
$results['details'][] = ['step' => '방문 비용 결제 확인', 'status' => 'FAIL', 'message' => $payment_result['message']];
|
||||
}
|
||||
} else {
|
||||
$results['details'][] = ['step' => '전문가 방문 요청', 'status' => 'FAIL', 'message' => $visit_result['message']];
|
||||
}
|
||||
} else {
|
||||
$results['details'][] = ['step' => '테스트 견적 확인', 'status' => 'INFO', 'message' => '전문가 방문 테스트용 견적이 없습니다.'];
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$results['success'] = false;
|
||||
$results['details'][] = ['step' => '오류', 'status' => 'FAIL', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function runFullIntegrationTest()
|
||||
{
|
||||
$results = [
|
||||
'success' => true,
|
||||
'message' => '전체 통합 테스트',
|
||||
'details' => []
|
||||
];
|
||||
|
||||
try {
|
||||
// 1. 고객 워크플로우 테스트
|
||||
$customer_test = runCustomerWorkflowTest();
|
||||
$results['details'][] = ['step' => '고객 워크플로우', 'status' => $customer_test['success'] ? 'PASS' : 'FAIL', 'message' => $customer_test['message']];
|
||||
|
||||
// 2. 대리점 워크플로우 테스트
|
||||
$dealer_test = runDealerWorkflowTest();
|
||||
$results['details'][] = ['step' => '대리점 워크플로우', 'status' => $dealer_test['success'] ? 'PASS' : 'FAIL', 'message' => $dealer_test['message']];
|
||||
|
||||
// 3. 관리자 워크플로우 테스트
|
||||
$admin_test = runAdminWorkflowTest();
|
||||
$results['details'][] = ['step' => '관리자 워크플로우', 'status' => $admin_test['success'] ? 'PASS' : 'FAIL', 'message' => $admin_test['message']];
|
||||
|
||||
// 4. 전문가 방문 워크플로우 테스트
|
||||
$expert_test = runExpertVisitWorkflowTest();
|
||||
$results['details'][] = ['step' => '전문가 방문 워크플로우', 'status' => $expert_test['success'] ? 'PASS' : 'FAIL', 'message' => $expert_test['message']];
|
||||
|
||||
// 전체 성공 여부 판단
|
||||
$all_passed = $customer_test['success'] && $dealer_test['success'] && $admin_test['success'] && $expert_test['success'];
|
||||
$results['success'] = $all_passed;
|
||||
|
||||
if ($all_passed) {
|
||||
$results['message'] = '모든 워크플로우 테스트가 성공적으로 완료되었습니다.';
|
||||
} else {
|
||||
$results['message'] = '일부 워크플로우 테스트에서 오류가 발생했습니다.';
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$results['success'] = false;
|
||||
$results['details'][] = ['step' => '통합 테스트 오류', 'status' => 'FAIL', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function cleanupTestData()
|
||||
{
|
||||
$results = [
|
||||
'success' => true,
|
||||
'message' => '테스트 데이터 정리',
|
||||
'details' => []
|
||||
];
|
||||
|
||||
try {
|
||||
// 테스트용 데이터 삭제
|
||||
$cleanup_queries = [
|
||||
"DELETE FROM estimate WHERE wr_id = 99999",
|
||||
"DELETE FROM estimate_item WHERE estimate_id IN (SELECT id FROM estimate WHERE wr_id = 99999)",
|
||||
"DELETE FROM estimate_bidding WHERE estimate_id IN (SELECT id FROM estimate WHERE wr_id = 99999)",
|
||||
"DELETE FROM estimate_history WHERE estimate_id IN (SELECT id FROM estimate WHERE wr_id = 99999)"
|
||||
];
|
||||
|
||||
foreach ($cleanup_queries as $query) {
|
||||
sql_query($query);
|
||||
}
|
||||
|
||||
$results['details'][] = ['step' => '테스트 데이터 삭제', 'status' => 'PASS', 'message' => '테스트 데이터가 정리되었습니다.'];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$results['success'] = false;
|
||||
$results['details'][] = ['step' => '정리 오류', 'status' => 'FAIL', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
전체 주문 관리 시스템의 워크플로우를 통합 테스트합니다.<br>
|
||||
고객, 대리점, 관리자, 전문가 방문의 각 시나리오를 자동으로 검증합니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 테스트 실행 버튼들 -->
|
||||
<div class="test-controls">
|
||||
<div class="btn-group">
|
||||
<form method="post" style="display: inline;">
|
||||
<input type="hidden" name="action" value="test_customer_workflow">
|
||||
<button type="submit" class="btn btn-primary">고객 워크플로우 테스트</button>
|
||||
</form>
|
||||
|
||||
<form method="post" style="display: inline;">
|
||||
<input type="hidden" name="action" value="test_dealer_workflow">
|
||||
<button type="submit" class="btn btn-info">대리점 워크플로우 테스트</button>
|
||||
</form>
|
||||
|
||||
<form method="post" style="display: inline;">
|
||||
<input type="hidden" name="action" value="test_admin_workflow">
|
||||
<button type="submit" class="btn btn-warning">관리자 워크플로우 테스트</button>
|
||||
</form>
|
||||
|
||||
<form method="post" style="display: inline;">
|
||||
<input type="hidden" name="action" value="test_expert_visit_workflow">
|
||||
<button type="submit" class="btn btn-success">전문가 방문 워크플로우 테스트</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" style="margin-top: 10px;">
|
||||
<form method="post" style="display: inline;">
|
||||
<input type="hidden" name="action" value="test_full_integration">
|
||||
<button type="submit" class="btn btn-danger">전체 통합 테스트</button>
|
||||
</form>
|
||||
|
||||
<form method="post" style="display: inline;">
|
||||
<input type="hidden" name="action" value="cleanup_test_data">
|
||||
<button type="submit" class="btn btn-secondary" onclick="return confirm('테스트 데이터를 정리하시겠습니까?')">테스트 데이터
|
||||
정리</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 테스트 결과 표시 -->
|
||||
<?php if (!empty($test_results)): ?>
|
||||
<div class="test-results">
|
||||
<h3>테스트 결과: <?php echo get_text($test_results['message']); ?></h3>
|
||||
|
||||
<div class="result-summary <?php echo $test_results['success'] ? 'success' : 'failure'; ?>">
|
||||
<strong><?php echo $test_results['success'] ? '✅ 성공' : '❌ 실패'; ?></strong>
|
||||
<p><?php echo get_text($test_results['message']); ?></p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($test_results['details'])): ?>
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>상세 테스트 결과</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">단계</th>
|
||||
<th scope="col">상태</th>
|
||||
<th scope="col">메시지</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($test_results['details'] as $detail): ?>
|
||||
<tr class="<?php echo strtolower($detail['status']); ?>">
|
||||
<td class="td_left"><?php echo get_text($detail['step']); ?></td>
|
||||
<td class="td_center">
|
||||
<span class="status-badge <?php echo strtolower($detail['status']); ?>">
|
||||
<?php echo $detail['status']; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="td_left"><?php echo get_text($detail['message']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 시스템 상태 체크 -->
|
||||
<div class="system-status">
|
||||
<h3>시스템 상태 체크</h3>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>시스템 구성 요소 상태</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">구성 요소</th>
|
||||
<th scope="col">상태</th>
|
||||
<th scope="col">설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// 시스템 상태 체크
|
||||
$system_checks = [
|
||||
[
|
||||
'component' => 'EstimateManager',
|
||||
'status' => class_exists('EstimateManager') ? 'OK' : 'ERROR',
|
||||
'description' => class_exists('EstimateManager') ? '견적 관리 클래스 로드됨' : '견적 관리 클래스 로드 실패'
|
||||
],
|
||||
[
|
||||
'component' => 'StatusManager',
|
||||
'status' => class_exists('StatusManager') ? 'OK' : 'ERROR',
|
||||
'description' => class_exists('StatusManager') ? '상태 관리 클래스 로드됨' : '상태 관리 클래스 로드 실패'
|
||||
],
|
||||
[
|
||||
'component' => 'NotificationHelper',
|
||||
'status' => class_exists('NotificationHelper') ? 'OK' : 'ERROR',
|
||||
'description' => class_exists('NotificationHelper') ? '알림 헬퍼 클래스 로드됨' : '알림 헬퍼 클래스 로드 실패'
|
||||
],
|
||||
[
|
||||
'component' => 'Database Tables',
|
||||
'status' => sql_fetch("SHOW TABLES LIKE 'estimate'") ? 'OK' : 'ERROR',
|
||||
'description' => sql_fetch("SHOW TABLES LIKE 'estimate'") ? '필수 테이블 존재' : '필수 테이블 누락'
|
||||
],
|
||||
[
|
||||
'component' => 'Mail Templates',
|
||||
'status' => sql_fetch("SHOW TABLES LIKE 'order_mail_templates'") ? 'OK' : 'ERROR',
|
||||
'description' => sql_fetch("SHOW TABLES LIKE 'order_mail_templates'") ? '메일 템플릿 테이블 존재' : '메일 템플릿 테이블 누락'
|
||||
],
|
||||
[
|
||||
'component' => 'SMS Templates',
|
||||
'status' => sql_fetch("SHOW TABLES LIKE 'order_sms_templates'") ? 'OK' : 'ERROR',
|
||||
'description' => sql_fetch("SHOW TABLES LIKE 'order_sms_templates'") ? 'SMS 템플릿 테이블 존재' : 'SMS 템플릿 테이블 누락'
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($system_checks as $check):
|
||||
?>
|
||||
<tr class="<?php echo strtolower($check['status']); ?>">
|
||||
<td class="td_left"><?php echo $check['component']; ?></td>
|
||||
<td class="td_center">
|
||||
<span class="status-badge <?php echo strtolower($check['status']); ?>">
|
||||
<?php echo $check['status']; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="td_left"><?php echo $check['description']; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.test-controls {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-group form {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007cba;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ffc107;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.result-summary {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.result-summary.success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.result-summary.failure {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-badge.pass {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.fail {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.ok {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
tr.pass {
|
||||
background-color: #d4edda;
|
||||
}
|
||||
|
||||
tr.fail {
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
tr.info {
|
||||
background-color: #d1ecf1;
|
||||
}
|
||||
|
||||
tr.ok {
|
||||
background-color: #d4edda;
|
||||
}
|
||||
|
||||
tr.error {
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
.system-status {
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
Reference in New Issue
Block a user