2633 lines
105 KiB
PHP
2633 lines
105 KiB
PHP
<?php
|
|
if (!defined('_GNUBOARD_'))
|
|
exit;
|
|
|
|
// StatusManager 클래스 로드
|
|
require_once G5_PATH . '/adm/order_manage/classes/StatusManager.class.php';
|
|
|
|
/**
|
|
* 견적 관리 클래스
|
|
*
|
|
* 견적 생성, 수정, 조회, 입찰 관리, 결제 계산 등 핵심 비즈니스 로직을 담당합니다.
|
|
* StatusManager와 연동하여 상태 관리를 체계적으로 처리합니다.
|
|
*/
|
|
class EstimateManager
|
|
{
|
|
private $estimate_table = 'estimate';
|
|
private $item_table = 'estimate_item';
|
|
private $bidding_table = 'estimate_bidding';
|
|
private $history_table = 'estimate_history';
|
|
private $member;
|
|
private $is_admin =false;
|
|
private $status_manager;
|
|
|
|
public function __construct()
|
|
{
|
|
global $member;
|
|
global $is_admin;
|
|
$this->member = $member;
|
|
$this->is_admin = $is_admin;
|
|
$this->status_manager = new StatusManager();
|
|
}
|
|
|
|
// ==============================================================================
|
|
// 핵심 견적 관리 메서드들 (Requirements 8.1, 8.2, 9.2, 10.3)
|
|
// StatusManager와 연동하여 체계적인 상태 관리 제공
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* 견적 생성 (StatusManager 연동)
|
|
*
|
|
* @param array $data 견적 데이터
|
|
* @param string $user_role 사용자 역할 (customer, agent, admin)
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function createEstimateWithStatus($data, $user_role = 'customer')
|
|
{
|
|
try {
|
|
sql_query("START TRANSACTION");
|
|
|
|
// 1. 기본 견적 생성
|
|
$estimate_id = $this->createEstimate($data);
|
|
if (!$estimate_id) {
|
|
throw new Exception('견적 생성에 실패했습니다.');
|
|
}
|
|
|
|
// 2. StatusManager를 통한 초기 상태 설정
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$data['wr_id'],
|
|
'견적신청중',
|
|
$user_id,
|
|
$user_role,
|
|
'견적 생성'
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 설정 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
sql_query("COMMIT");
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate_id,
|
|
'wr_id' => $data['wr_id'],
|
|
'status' => '견적신청중'
|
|
],
|
|
'message' => '견적이 성공적으로 생성되었습니다.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
sql_query("ROLLBACK");
|
|
error_log("EstimateManager::createEstimateWithStatus Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 견적 완료 처리 (StatusManager 연동)
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function completeEstimate($wr_id, $user_role = 'customer')
|
|
{
|
|
try {
|
|
// 1. 견적 데이터 검증
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적을 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 견적 항목 존재 확인
|
|
$item_count = sql_fetch("SELECT COUNT(*) as cnt FROM {$this->item_table} WHERE estimate_id = '{$estimate['id']}' AND is_deleted = 0");
|
|
if (!$item_count || $item_count['cnt'] == 0) {
|
|
throw new Exception('견적 항목이 없습니다. 최소 1개 이상의 항목을 추가해주세요.');
|
|
}
|
|
|
|
// 3. StatusManager를 통한 상태 변경
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$wr_id,
|
|
'작성완료',
|
|
$user_id,
|
|
$user_role,
|
|
'고객 견적 작성 완료'
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
// ❗ [핵심 수정] '작성완료' 상태 변경 후 알림을 발송합니다.
|
|
if (function_exists('notify_by_status')) {
|
|
notify_by_status($wr_id, '작성완료');
|
|
}
|
|
|
|
// 4. 24시간 입찰 마감시간 설정
|
|
$deadline = date('Y-m-d H:i:s', strtotime('+24 hours'));
|
|
$update_result = $this->update($this->estimate_table, $estimate['id'], [
|
|
'bidding_deadline' => $deadline,
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
if (!$update_result) {
|
|
throw new Exception('입찰 마감시간 설정에 실패했습니다.');
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'status' => '작성완료',
|
|
'bidding_deadline' => $deadline
|
|
],
|
|
'message' => '견적 작성이 완료되었습니다. 24시간 후에 견적을 확인해주세요.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::completeEstimate Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 새 견적을 생성합니다 (Requirement 8.1, 8.2)
|
|
* @param array $data 견적 데이터
|
|
* @return int|bool estimate_id 또는 실패 시 false
|
|
*/
|
|
public function createEstimate($data)
|
|
{
|
|
sql_query("START TRANSACTION");
|
|
try {
|
|
$customer_id = $this->member['mb_id'] ?? '';
|
|
|
|
$estimate_data = [
|
|
'wr_id' => $data['wr_id'],
|
|
'status' => '수정중',
|
|
'zip_code' => trim($data['zip_code'] ?? ''),
|
|
'address1' => trim($data['address1'] ?? ''),
|
|
'address2' => trim($data['address2'] ?? ''),
|
|
'address3' => trim($data['address3'] ?? ''),
|
|
'house_type' => trim($data['house_type'] ?? ''),
|
|
'house_size' => trim($data['house_size'] ?? ''),
|
|
'material' => trim($data['material'] ?? ''),
|
|
'color' => trim($data['color'] ?? ''),
|
|
'glass_thickness' => trim($data['glass_thickness'] ?? ''),
|
|
'install' => trim($data['install'] ?? ''),
|
|
'created_by' => $customer_id,
|
|
'updated_by' => $customer_id,
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
$estimate_id = $this->create($this->estimate_table, $estimate_data);
|
|
if (!$estimate_id) {
|
|
throw new Exception('견적서 생성 실패');
|
|
}
|
|
|
|
// 견적 항목들 저장
|
|
if (isset($data['items']) && is_array($data['items'])) {
|
|
foreach ($data['items'] as $index => $item) {
|
|
$item['estimate_id'] = $estimate_id;
|
|
$item['no'] = $index + 1;
|
|
$this->createEstimateItem($estimate_id, $item);
|
|
}
|
|
}
|
|
|
|
$this->logHistory($estimate_id, null, 'estimate_created', $estimate_data);
|
|
|
|
sql_query("COMMIT");
|
|
return $estimate_id;
|
|
|
|
} catch (Exception $e) {
|
|
sql_query("ROLLBACK");
|
|
error_log("createEstimate 오류: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 견적 선택 (StatusManager 연동 개선)
|
|
*
|
|
* @param int $estimate_id 견적서 ID
|
|
* @param int $bid_id 선택할 입찰 ID
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function selectBidWithStatus($estimate_id, $bid_id, $user_role = 'customer')
|
|
{
|
|
try {
|
|
sql_query("START TRANSACTION");
|
|
|
|
// 1. 견적서와 입찰 정보 확인
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE id = '{$estimate_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
$bid = sql_fetch("SELECT * FROM {$this->bidding_table} WHERE id = '{$bid_id}' AND estimate_id = '{$estimate_id}'");
|
|
if (!$bid) {
|
|
throw new Exception('입찰 정보를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 24시간 경과 확인
|
|
if ($estimate['bidding_deadline'] && strtotime($estimate['bidding_deadline']) > time()) {
|
|
throw new Exception('아직 입찰 마감시간이 되지 않았습니다.');
|
|
}
|
|
|
|
// 3. 선택된 입찰의 답변글 ID 찾기
|
|
$reply_write = sql_fetch("
|
|
SELECT wr_id FROM g5_write_order
|
|
WHERE wr_parent = '{$estimate['wr_id']}'
|
|
AND mb_id = '{$bid['dealer_id']}'
|
|
AND wr_1 = '견적제안'
|
|
");
|
|
|
|
if (!$reply_write) {
|
|
throw new Exception('해당 견적의 답변글을 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 4. StatusManager를 통한 답변글 상태 변경 (견적채택)
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$reply_write['wr_id'],
|
|
'견적채택',
|
|
$user_id,
|
|
$user_role,
|
|
"견적 선택 - 대리점: {$bid['dealer_id']}"
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('견적 상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
// 5. 견적서 테이블 업데이트
|
|
$update_result = $this->update($this->estimate_table, $estimate_id, [
|
|
'selected_bid_id' => $bid_id,
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
if (!$update_result) {
|
|
throw new Exception('견적서 업데이트에 실패했습니다.');
|
|
}
|
|
|
|
// 6. 입찰 상태 업데이트
|
|
sql_query("UPDATE {$this->bidding_table} SET status = 'rejected' WHERE estimate_id = '{$estimate_id}'");
|
|
sql_query("UPDATE {$this->bidding_table} SET status = 'selected' WHERE id = '{$bid_id}'");
|
|
|
|
// 7. 결제 정보 계산
|
|
$payment_info = $this->calculatePaymentAmount($estimate_id, 'deposit');
|
|
|
|
// 8. 대리점 알림 발송
|
|
$this->sendDealerSelectedNotification($estimate['wr_id'], $bid['dealer_id'], $bid);
|
|
|
|
sql_query("COMMIT");
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate_id,
|
|
'selected_bid_id' => $bid_id,
|
|
'dealer_id' => $bid['dealer_id'],
|
|
'total_amount' => $bid['total_amount'],
|
|
'payment_info' => $payment_info
|
|
],
|
|
'message' => '견적이 선택되었습니다. 계약금을 입금해주세요.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
sql_query("ROLLBACK");
|
|
error_log('EstimateManager::selectBidWithStatus Error: ' . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 호환성을 위한 selectBid 메서드 (Deprecated)
|
|
* @deprecated selectBidWithStatus 사용 권장
|
|
*/
|
|
public function selectBid($estimate_id, $bid_id)
|
|
{
|
|
$result = $this->selectBidWithStatus($estimate_id, $bid_id, 'customer');
|
|
return $result['success'];
|
|
}
|
|
|
|
/**
|
|
* 결제 금액 계산 (개선된 버전)
|
|
*
|
|
* @param int $estimate_id 견적서 ID
|
|
* @param string $type 결제 타입 ('deposit', 'interim', 'final')
|
|
* @return array|false 결제 정보 배열 또는 실패 시 false
|
|
*/
|
|
public function calculatePaymentAmount($estimate_id, $type = 'deposit')
|
|
{
|
|
try {
|
|
// 1. 견적서 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE id = '{$estimate_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 선택된 입찰 정보 조회
|
|
if (!$estimate['selected_bid_id']) {
|
|
throw new Exception('선택된 입찰이 없습니다.');
|
|
}
|
|
|
|
$bid = sql_fetch("SELECT * FROM {$this->bidding_table} WHERE id = '{$estimate['selected_bid_id']}'");
|
|
if (!$bid) {
|
|
throw new Exception('선택된 입찰 정보를 찾을 수 없습니다.');
|
|
}
|
|
|
|
$total_amount = (int) $bid['total_amount'];
|
|
|
|
// 3. 설정에서 결제 비율 조회
|
|
$config = $this->getOrderConfig();
|
|
$deposit_rate = (float) ($config['contract_deposit_rate'] ?? 10);
|
|
$interim_rate = (float) ($config['middle_payment_rate'] ?? 40);
|
|
|
|
// 잔금 비율은 자동 계산 (100 - 계약금 - 중도금)
|
|
$final_rate = 100 - $deposit_rate - $interim_rate;
|
|
|
|
// 4. 각 단계별 금액 계산
|
|
$deposit_amount = (int) ($total_amount * $deposit_rate / 100);
|
|
$interim_amount = (int) ($total_amount * $interim_rate / 100);
|
|
$final_amount = $total_amount - $deposit_amount - $interim_amount;
|
|
|
|
$payment_info = [
|
|
'estimate_id' => $estimate_id,
|
|
'total_amount' => $total_amount,
|
|
'deposit_amount' => $deposit_amount,
|
|
'interim_amount' => $interim_amount,
|
|
'final_amount' => $final_amount,
|
|
'deposit_rate' => $deposit_rate,
|
|
'interim_rate' => $interim_rate,
|
|
'final_rate' => $final_rate,
|
|
'dealer_id' => $bid['dealer_id'],
|
|
'selected_bid_id' => $bid['id']
|
|
];
|
|
|
|
// 5. 요청된 타입에 따른 현재 결제 금액 설정
|
|
switch ($type) {
|
|
case 'deposit':
|
|
$payment_info['current_amount'] = $deposit_amount;
|
|
$payment_info['current_type'] = '계약금';
|
|
break;
|
|
case 'interim':
|
|
$payment_info['current_amount'] = $interim_amount;
|
|
$payment_info['current_type'] = '중도금';
|
|
break;
|
|
case 'final':
|
|
$payment_info['current_amount'] = $final_amount;
|
|
$payment_info['current_type'] = '잔금';
|
|
break;
|
|
default:
|
|
$payment_info['current_amount'] = $deposit_amount;
|
|
$payment_info['current_type'] = '계약금';
|
|
}
|
|
|
|
// 6. 계좌 정보 추가
|
|
$payment_info['account_info'] = $config['payment_account_info'] ?? '계좌 정보가 설정되지 않았습니다.';
|
|
|
|
return $payment_info;
|
|
|
|
} catch (Exception $e) {
|
|
error_log('EstimateManager::calculatePaymentAmount Error: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 결제 상태 업데이트 (StatusManager 연동)
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $payment_type 결제 타입 (deposit, interim, final)
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function updatePaymentStatus($wr_id, $payment_type, $user_role = 'admin')
|
|
{
|
|
try {
|
|
// 결제 타입에 따른 상태 매핑
|
|
$status_map = [
|
|
'deposit' => '입금확인',
|
|
'interim' => '중도금입금완료',
|
|
'final' => '잔금입금완료'
|
|
];
|
|
|
|
if (!isset($status_map[$payment_type])) {
|
|
throw new Exception('유효하지 않은 결제 타입입니다.');
|
|
}
|
|
|
|
$new_status = $status_map[$payment_type];
|
|
|
|
// StatusManager를 통한 상태 변경
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$wr_id,
|
|
$new_status,
|
|
$user_id,
|
|
$user_role,
|
|
"{$payment_type} 결제 확인"
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'wr_id' => $wr_id,
|
|
'payment_type' => $payment_type,
|
|
'new_status' => $new_status
|
|
],
|
|
'message' => "{$payment_type} 결제가 확인되었습니다."
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::updatePaymentStatus Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 견적 상세 정보 조회 (권한별 필터링)
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $user_role 사용자 역할
|
|
* @param string $user_id 사용자 ID
|
|
* @return array|false 견적 정보 또는 실패 시 false
|
|
*/
|
|
public function getEstimateDetails($wr_id, $user_role = 'guest', $user_id = '')
|
|
{
|
|
try {
|
|
// 1. 견적 기본 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적을 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 견적 항목 조회
|
|
$items_sql = "SELECT * FROM {$this->item_table} WHERE estimate_id = '{$estimate['id']}' AND is_deleted = 0 ORDER BY no ASC";
|
|
$items_result = sql_query($items_sql);
|
|
$items = [];
|
|
|
|
while ($item = sql_fetch_array($items_result)) {
|
|
// 권한별 가격 정보 필터링
|
|
if (
|
|
$user_role === 'admin' ||
|
|
($user_role === 'agent' && $item['dealer_id'] === $user_id) ||
|
|
($user_role === 'customer' && empty($item['dealer_id']))
|
|
) {
|
|
$items[] = $item;
|
|
} else {
|
|
// 가격 정보 숨김
|
|
$item['price'] = null;
|
|
$item['amount'] = null;
|
|
$items[] = $item;
|
|
}
|
|
}
|
|
|
|
// 3. 입찰 정보 조회 (권한별)
|
|
$biddings = [];
|
|
if ($user_role === 'admin' || $user_role === 'customer') {
|
|
$biddings = $this->getBiddingList($estimate['id']);
|
|
} elseif ($user_role === 'agent') {
|
|
// 대리점은 자신의 입찰만 조회
|
|
$biddings_sql = "SELECT * FROM {$this->bidding_table} WHERE estimate_id = '{$estimate['id']}' AND dealer_id = '{$user_id}'";
|
|
$biddings_result = sql_query($biddings_sql);
|
|
while ($bid = sql_fetch_array($biddings_result)) {
|
|
$biddings[] = $bid;
|
|
}
|
|
}
|
|
|
|
// 4. 현재 상태 정보
|
|
$status_info = $this->status_manager->getCurrentStatus($wr_id);
|
|
|
|
// 5. 결제 정보 (선택된 견적이 있는 경우)
|
|
$payment_info = null;
|
|
if ($estimate['selected_bid_id']) {
|
|
$payment_info = $this->calculatePaymentAmount($estimate['id']);
|
|
}
|
|
|
|
return [
|
|
'estimate' => $estimate,
|
|
'items' => $items,
|
|
'biddings' => $biddings,
|
|
'status_info' => $status_info,
|
|
'payment_info' => $payment_info,
|
|
'user_permissions' => [
|
|
'can_edit' => $this->canEditEstimate($estimate, $user_role, $user_id),
|
|
'can_bid' => $this->canBidOnEstimate($estimate, $user_role, $user_id),
|
|
'can_select' => $this->canSelectBid($estimate, $user_role, $user_id)
|
|
]
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getEstimateDetails Error: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 견적 수정 권한 확인
|
|
*/
|
|
private function canEditEstimate($estimate, $user_role, $user_id)
|
|
{
|
|
if ($user_role === 'admin')
|
|
return true;
|
|
if ($user_role === 'customer' && $estimate['created_by'] === $user_id && ($estimate['status'] === '견적신청중' || $estimate['status'] === 'requesting'))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 입찰 권한 확인
|
|
*/
|
|
private function canBidOnEstimate($estimate, $user_role, $user_id)
|
|
{
|
|
if ($user_role !== 'agent')
|
|
return false;
|
|
if ($estimate['status'] !== '작성완료')
|
|
return false;
|
|
if ($estimate['bidding_deadline'] && strtotime($estimate['bidding_deadline']) < time())
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 견적 선택 권한 확인
|
|
*/
|
|
private function canSelectBid($estimate, $user_role, $user_id)
|
|
{
|
|
if ($user_role !== 'customer')
|
|
return false;
|
|
if ($estimate['created_by'] !== $user_id)
|
|
return false;
|
|
if ($estimate['status'] !== '작성완료')
|
|
return false;
|
|
if ($estimate['bidding_deadline'] && strtotime($estimate['bidding_deadline']) > time())
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// ==============================================================================
|
|
// 대리점 입찰 관리 기능 (Requirement 9.2) - StatusManager 연동
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* 대리점 입찰 생성 (개선된 버전)
|
|
*
|
|
* @param int $estimate_id 견적서 ID
|
|
* @param array $bid_data 입찰 데이터
|
|
* @param array $items_data 견적 항목 데이터
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function createBiddingWithItems($estimate_id, $bid_data, $items_data = [])
|
|
{
|
|
try {
|
|
sql_query("START TRANSACTION");
|
|
|
|
$dealer_id = $this->member['mb_id'] ?? '';
|
|
if (empty($dealer_id)) {
|
|
throw new Exception('대리점 정보가 없습니다.');
|
|
}
|
|
|
|
// 1. 견적서 존재 및 입찰 가능 여부 확인
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE id = '{$estimate_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
if ($estimate['status'] !== '작성완료') {
|
|
throw new Exception('입찰 가능한 상태가 아닙니다.');
|
|
}
|
|
|
|
// 2. 24시간 입찰 마감시간 확인
|
|
if ($estimate['bidding_deadline'] && strtotime($estimate['bidding_deadline']) < time()) {
|
|
throw new Exception('입찰 마감시간이 지났습니다.');
|
|
}
|
|
|
|
// 3. 시스템 비용 계산
|
|
$dealer_level = $this->member['mb_level'] ?? 5;
|
|
$commission_rate = $this->getDealerCommissionRate($dealer_level);
|
|
|
|
$base_amount = (int) ($bid_data['total_amount'] ?? 0);
|
|
$commission_amount = (int) ($base_amount * $commission_rate / 100);
|
|
$final_amount = $base_amount + $commission_amount;
|
|
|
|
// 4. 기존 입찰 확인 (수정 모드)
|
|
$existing_bid = sql_fetch("SELECT id FROM {$this->bidding_table} WHERE estimate_id = '{$estimate_id}' AND dealer_id = '{$dealer_id}'");
|
|
|
|
$bidding_data = [
|
|
'estimate_id' => $estimate_id,
|
|
'dealer_id' => $dealer_id,
|
|
'bid_amount' => $base_amount,
|
|
'commission_rate' => $commission_rate,
|
|
'commission_amount' => $commission_amount,
|
|
'total_amount' => $final_amount,
|
|
'bid_message' => trim($bid_data['message'] ?? ''),
|
|
'status' => 'bidding',
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
if ($existing_bid) {
|
|
// 기존 입찰 수정
|
|
$bidding_id = $existing_bid['id'];
|
|
$this->update($this->bidding_table, $bidding_id, $bidding_data);
|
|
$action = 'bidding_updated';
|
|
} else {
|
|
// 새 입찰 생성
|
|
$bidding_id = $this->create($this->bidding_table, $bidding_data);
|
|
$action = 'bidding_created';
|
|
}
|
|
|
|
if (!$bidding_id) {
|
|
throw new Exception('입찰 저장에 실패했습니다.');
|
|
}
|
|
|
|
// 5. 견적 항목 저장 (대리점 정보 포함)
|
|
if (!empty($items_data)) {
|
|
// 기존 대리점 항목 삭제
|
|
sql_query("DELETE FROM {$this->item_table} WHERE estimate_id = '{$estimate_id}' AND dealer_id = '{$dealer_id}'");
|
|
|
|
foreach ($items_data as $index => $item) {
|
|
$item_data = [
|
|
'estimate_id' => $estimate_id,
|
|
'dealer_id' => $dealer_id,
|
|
'no' => $index + 1,
|
|
'product' => trim($item['product'] ?? ''),
|
|
'qty' => (int) ($item['qty'] ?? 1),
|
|
'price' => (int) ($item['price'] ?? 0),
|
|
'amount' => (int) ($item['amount'] ?? 0),
|
|
'brand' => trim($item['brand'] ?? ''),
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'created_by' => $dealer_id
|
|
];
|
|
|
|
$this->create($this->item_table, $item_data);
|
|
}
|
|
}
|
|
|
|
// 6. 게시판 답변글 상태 업데이트 (있는 경우)
|
|
$reply_write = sql_fetch("
|
|
SELECT wr_id FROM g5_write_order
|
|
WHERE wr_parent = '{$estimate['wr_id']}'
|
|
AND mb_id = '{$dealer_id}'
|
|
");
|
|
|
|
if ($reply_write) {
|
|
sql_query("UPDATE g5_write_order SET wr_1 = '견적제안' WHERE wr_id = '{$reply_write['wr_id']}'");
|
|
}
|
|
|
|
$this->logHistory($estimate_id, null, $action, [
|
|
'bidding_id' => $bidding_id,
|
|
'dealer_id' => $dealer_id,
|
|
'base_amount' => $base_amount,
|
|
'commission_rate' => $commission_rate,
|
|
'total_amount' => $final_amount
|
|
]);
|
|
|
|
sql_query("COMMIT");
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'bidding_id' => $bidding_id,
|
|
'estimate_id' => $estimate_id,
|
|
'dealer_id' => $dealer_id,
|
|
'base_amount' => $base_amount,
|
|
'commission_amount' => $commission_amount,
|
|
'total_amount' => $final_amount,
|
|
'commission_rate' => $commission_rate
|
|
],
|
|
'message' => '견적 제안이 성공적으로 저장되었습니다.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
sql_query("ROLLBACK");
|
|
error_log('EstimateManager::createBiddingWithItems Error: ' . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 대리점 수수료율 조회
|
|
*
|
|
* @param int $dealer_level 대리점 레벨
|
|
* @return float 수수료율 (%)
|
|
*/
|
|
private function getDealerCommissionRate($dealer_level)
|
|
{
|
|
$config = $this->getOrderConfig();
|
|
|
|
switch ($dealer_level) {
|
|
case 5:
|
|
return (float) ($config['dealer_commission_level_5'] ?? 5);
|
|
case 6:
|
|
return (float) ($config['dealer_commission_level_6'] ?? 11);
|
|
case 7:
|
|
return (float) ($config['dealer_commission_level_7'] ?? 19);
|
|
default:
|
|
return 5.0; // 기본값
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 호환성을 위한 createBidding 메서드 (Deprecated)
|
|
* @deprecated createBiddingWithItems 사용 권장
|
|
*/
|
|
public function createBidding($estimate_id, $bid_data)
|
|
{
|
|
$result = $this->createBiddingWithItems($estimate_id, $bid_data, []);
|
|
return $result['success'] ? $result['data']['bidding_id'] : false;
|
|
}
|
|
|
|
// ==============================================================================
|
|
// 시공 일정 관리 기능 (Task 5.2 - Requirements 4.2, 9.4)
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* 시공 일정 설정
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $construction_date 시공 예정일 (Y-m-d)
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function setConstructionSchedule($wr_id, $construction_date, $user_role = 'agent')
|
|
{
|
|
try {
|
|
// 1. 견적서 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 선택된 입찰이 있는지 확인
|
|
if (!$estimate['selected_bid_id']) {
|
|
throw new Exception('선택된 견적이 없습니다.');
|
|
}
|
|
|
|
$bid = sql_fetch("SELECT * FROM {$this->bidding_table} WHERE id = '{$estimate['selected_bid_id']}'");
|
|
if (!$bid) {
|
|
throw new Exception('선택된 견적 정보를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 3. 권한 확인 (대리점은 자신의 견적만, 관리자는 모든 견적)
|
|
if ($user_role === 'agent') {
|
|
$dealer_id = $this->member['mb_id'] ?? '';
|
|
if ($bid['dealer_id'] !== $dealer_id) {
|
|
throw new Exception('해당 견적에 대한 권한이 없습니다.');
|
|
}
|
|
}
|
|
|
|
// 4. 시공일 유효성 검증
|
|
$construction_timestamp = strtotime($construction_date);
|
|
if (!$construction_timestamp || $construction_timestamp < strtotime('today')) {
|
|
throw new Exception('유효하지 않은 시공 예정일입니다.');
|
|
}
|
|
|
|
// 5. 중도금 입금 예정일 계산 (시공 2일 전)
|
|
$interim_payment_date = date('Y-m-d', strtotime($construction_date . ' -2 days'));
|
|
|
|
// 6. 견적서 업데이트 (temp 컬럼 활용)
|
|
$update_data = [
|
|
'temp_6' => $construction_date, // 시공 예정일
|
|
'temp_7' => $interim_payment_date, // 중도금 입금 예정일
|
|
'temp_8' => date('Y-m-d H:i:s'), // 일정 설정 시간
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$update_result = $this->update($this->estimate_table, $estimate['id'], $update_data);
|
|
if (!$update_result) {
|
|
throw new Exception('시공 일정 저장에 실패했습니다.');
|
|
}
|
|
|
|
// 7. 상태 변경 (계약금입금완료 → 중도금입금예정)
|
|
$current_status = $this->status_manager->getCurrentStatus($wr_id);
|
|
if ($current_status && $current_status['status'] === '입금확인') {
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$this->status_manager->changeStatus(
|
|
$wr_id,
|
|
'중도금입금예정',
|
|
$user_id,
|
|
$user_role,
|
|
"시공 일정 설정: {$construction_date}"
|
|
);
|
|
}
|
|
|
|
// 8. 이력 기록
|
|
$this->logHistory($estimate['id'], null, 'construction_scheduled', [
|
|
'construction_date' => $construction_date,
|
|
'interim_payment_date' => $interim_payment_date,
|
|
'dealer_id' => $bid['dealer_id']
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'construction_date' => $construction_date,
|
|
'interim_payment_date' => $interim_payment_date,
|
|
'dealer_id' => $bid['dealer_id']
|
|
],
|
|
'message' => '시공 일정이 설정되었습니다.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::setConstructionSchedule Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 시공 일정 조회
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @return array|false 시공 일정 정보 또는 실패 시 false
|
|
*/
|
|
public function getConstructionSchedule($wr_id)
|
|
{
|
|
try {
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
return false;
|
|
}
|
|
|
|
$schedule_info = [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'construction_date' => $estimate['temp_6'] ?? null,
|
|
'interim_payment_date' => $estimate['temp_7'] ?? null,
|
|
'schedule_set_at' => $estimate['temp_8'] ?? null,
|
|
'has_schedule' => !empty($estimate['temp_6'])
|
|
];
|
|
|
|
// 선택된 대리점 정보 추가
|
|
if ($estimate['selected_bid_id']) {
|
|
$bid = sql_fetch("SELECT dealer_id FROM {$this->bidding_table} WHERE id = '{$estimate['selected_bid_id']}'");
|
|
if ($bid) {
|
|
$dealer = sql_fetch("SELECT mb_name, mb_hp FROM g5_member WHERE mb_id = '{$bid['dealer_id']}'");
|
|
$schedule_info['dealer_id'] = $bid['dealer_id'];
|
|
$schedule_info['dealer_name'] = $dealer['mb_name'] ?? '';
|
|
$schedule_info['dealer_phone'] = $dealer['mb_hp'] ?? '';
|
|
}
|
|
}
|
|
|
|
return $schedule_info;
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getConstructionSchedule Error: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 중도금 입금 타이밍 확인
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @return array ['is_due' => bool, 'days_remaining' => int, 'due_date' => string]
|
|
*/
|
|
public function checkInterimPaymentTiming($wr_id)
|
|
{
|
|
try {
|
|
$schedule = $this->getConstructionSchedule($wr_id);
|
|
if (!$schedule || !$schedule['interim_payment_date']) {
|
|
return [
|
|
'is_due' => false,
|
|
'days_remaining' => null,
|
|
'due_date' => null,
|
|
'message' => '시공 일정이 설정되지 않았습니다.'
|
|
];
|
|
}
|
|
|
|
$due_date = $schedule['interim_payment_date'];
|
|
$due_timestamp = strtotime($due_date);
|
|
$today_timestamp = strtotime('today');
|
|
|
|
$days_remaining = (int) (($due_timestamp - $today_timestamp) / (24 * 60 * 60));
|
|
$is_due = $days_remaining <= 0;
|
|
|
|
return [
|
|
'is_due' => $is_due,
|
|
'days_remaining' => $days_remaining,
|
|
'due_date' => $due_date,
|
|
'construction_date' => $schedule['construction_date'],
|
|
'message' => $is_due ? '중도금 입금 기한이 도래했습니다.' : "중도금 입금까지 {$days_remaining}일 남았습니다."
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::checkInterimPaymentTiming Error: " . $e->getMessage());
|
|
return [
|
|
'is_due' => false,
|
|
'days_remaining' => null,
|
|
'due_date' => null,
|
|
'message' => '중도금 입금 타이밍 확인 중 오류가 발생했습니다.'
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 대리점별 시공 일정 목록 조회
|
|
*
|
|
* @param string $dealer_id 대리점 ID (null이면 전체)
|
|
* @param string $status_filter 상태 필터
|
|
* @return array 시공 일정 목록
|
|
*/
|
|
public function getConstructionScheduleList($dealer_id = null, $status_filter = null)
|
|
{
|
|
try {
|
|
$where_conditions = ["e.is_deleted = 0", "e.selected_bid_id IS NOT NULL", "e.temp_6 IS NOT NULL"];
|
|
|
|
if ($dealer_id) {
|
|
$where_conditions[] = "b.dealer_id = '{$dealer_id}'";
|
|
}
|
|
|
|
if ($status_filter) {
|
|
$where_conditions[] = "w.wr_1 = '{$status_filter}'";
|
|
}
|
|
|
|
$where_clause = implode(' AND ', $where_conditions);
|
|
|
|
$sql = "
|
|
SELECT
|
|
e.id as estimate_id,
|
|
e.wr_id,
|
|
e.temp_6 as construction_date,
|
|
e.temp_7 as interim_payment_date,
|
|
e.temp_8 as schedule_set_at,
|
|
b.dealer_id,
|
|
b.total_amount,
|
|
m.mb_name as dealer_name,
|
|
m.mb_hp as dealer_phone,
|
|
w.wr_subject,
|
|
w.wr_1 as status,
|
|
w.mb_id as customer_id,
|
|
cm.mb_name as customer_name,
|
|
cm.mb_hp as customer_phone
|
|
FROM {$this->estimate_table} e
|
|
LEFT JOIN {$this->bidding_table} b ON e.selected_bid_id = b.id
|
|
LEFT JOIN g5_member m ON b.dealer_id = m.mb_id
|
|
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
|
|
WHERE {$where_clause}
|
|
ORDER BY e.temp_6 ASC
|
|
";
|
|
|
|
$result = sql_query($sql);
|
|
$schedules = [];
|
|
|
|
while ($row = sql_fetch_array($result)) {
|
|
// 중도금 입금 타이밍 정보 추가
|
|
$timing_info = $this->checkInterimPaymentTiming($row['wr_id']);
|
|
$row['interim_payment_timing'] = $timing_info;
|
|
|
|
$schedules[] = $row;
|
|
}
|
|
|
|
return $schedules;
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getConstructionScheduleList Error: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 시공 완료 처리
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $completion_notes 완료 메모
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function completeConstruction($wr_id, $completion_notes = '', $user_role = 'agent')
|
|
{
|
|
try {
|
|
// 1. 견적서 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 권한 확인
|
|
if ($user_role === 'agent') {
|
|
$dealer_id = $this->member['mb_id'] ?? '';
|
|
$bid = sql_fetch("SELECT dealer_id FROM {$this->bidding_table} WHERE id = '{$estimate['selected_bid_id']}'");
|
|
if (!$bid || $bid['dealer_id'] !== $dealer_id) {
|
|
throw new Exception('해당 견적에 대한 권한이 없습니다.');
|
|
}
|
|
}
|
|
|
|
// 3. 완료 정보 저장 (temp 컬럼 활용)
|
|
$update_data = [
|
|
'temp_9' => date('Y-m-d'), // 시공 완료일
|
|
'temp_10' => $completion_notes, // 완료 메모
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$update_result = $this->update($this->estimate_table, $estimate['id'], $update_data);
|
|
if (!$update_result) {
|
|
throw new Exception('시공 완료 정보 저장에 실패했습니다.');
|
|
}
|
|
|
|
// 4. 상태 변경 (시공완료)
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$wr_id,
|
|
'시공완료',
|
|
$user_id,
|
|
$user_role,
|
|
"시공 완료: {$completion_notes}"
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
// 5. 이력 기록
|
|
$this->logHistory($estimate['id'], null, 'construction_completed', [
|
|
'completion_date' => date('Y-m-d'),
|
|
'completion_notes' => $completion_notes,
|
|
'completed_by' => $user_id
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'completion_date' => date('Y-m-d'),
|
|
'completion_notes' => $completion_notes
|
|
],
|
|
'message' => '시공이 완료되었습니다.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::completeConstruction Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 견적서의 모든 입찰 조회
|
|
* @param int $estimate_id 견적서 ID
|
|
* @return array 입찰 목록
|
|
*/
|
|
public function getBiddingList($estimate_id)
|
|
{
|
|
$sql = "SELECT b.*, m.mb_name as dealer_name
|
|
FROM {$this->bidding_table} b
|
|
LEFT JOIN g5_member m ON b.dealer_id = m.mb_id
|
|
WHERE b.estimate_id = '{$estimate_id}'
|
|
ORDER BY b.created_at ASC";
|
|
|
|
$result = sql_query($sql);
|
|
$biddings = [];
|
|
|
|
while ($row = sql_fetch_array($result)) {
|
|
$biddings[] = $row;
|
|
}
|
|
|
|
return $biddings;
|
|
}
|
|
|
|
// ==============================================================================
|
|
// 기존 메서드들 (호환성 유지)
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* 고객의 견적 요청을 생성하거나 업데이트합니다.
|
|
* @param int $wr_id 게시물 ID
|
|
* @param array $post_data 폼 데이터
|
|
* @return int|bool estimate_id 또는 실패 시 false
|
|
*/
|
|
|
|
public function saveCustomerRequest($wr_id, $post_data)
|
|
{
|
|
$_POST['from_save_estimate'] = true;
|
|
$this->logHistory($wr_id, null, 'saveCustomerRequest', [$post_data]);
|
|
sql_query("START TRANSACTION");
|
|
try {
|
|
$estimate = sql_fetch("SELECT id FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}'");
|
|
|
|
error_log("saveCustomerRequest 호출 - wr_id: {$wr_id}, POST 데이터: " . json_encode($post_data));
|
|
|
|
$customer_id = $this->member['mb_id'] ?? '';
|
|
|
|
$estimate_data = [
|
|
'wr_id' => $wr_id,
|
|
'zip_code' => trim($post_data['zip_code'] ?? ''),
|
|
'address1' => trim($post_data['address1'] ?? ''),
|
|
'address2' => trim($post_data['address2'] ?? ''),
|
|
'address3' => trim($post_data['address3'] ?? ''),
|
|
'house_type' => trim($post_data['house_type'] ?? ''),
|
|
'house_size' => trim($post_data['house_size'] ?? ''),
|
|
'material' => trim($post_data['material'] ?? ''),
|
|
'color' => trim($post_data['common_color'] ?? ''),
|
|
'temp_2' => trim($post_data['common_color_custom'] ?? ''),
|
|
'glass_thickness' => trim($post_data['common_glass_thickness'] ?? ''),
|
|
'install' => trim($post_data['construction_main'] ?? ''),
|
|
'temp_1' => trim($post_data['product_main'] ?? ''),
|
|
'updated_by' => $customer_id,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
if ($estimate) {
|
|
$estimate_id = $estimate['id'];
|
|
$this->update($this->estimate_table, $estimate_id, $estimate_data);
|
|
} else {
|
|
// 💡 [수정] 신규 생성 시에만 status와 created_by, created_at를 추가
|
|
$estimate_data['status'] = '견적신청중';
|
|
$estimate_data['created_by'] = $customer_id;
|
|
$estimate_data['created_at'] = date('Y-m-d H:i:s');
|
|
|
|
$estimate_id = $this->create($this->estimate_table, $estimate_data);
|
|
}
|
|
|
|
$this->logHistory($wr_id, null, 'saveCustomerRequest', ['estimate_data' => $estimate_data, 'estimate_id' => $estimate_id]);
|
|
|
|
if (!$estimate_id)
|
|
throw new Exception('견적서 생성/조회 실패');
|
|
|
|
// 삭제 대상이 될 아이템들의 현재 상태를 확인하기 위해 로그를 추가합니다.
|
|
$items_before_delete_sql = "SELECT * FROM {$this->item_table} WHERE estimate_id = '{$estimate_id}'";
|
|
$items_before_delete_result = sql_query($items_before_delete_sql);
|
|
$items_to_check = [];
|
|
while ($item = sql_fetch_array($items_before_delete_result)) {
|
|
$items_to_check[] = $item;
|
|
}
|
|
$this->logHistory($wr_id, null, 'ITEMS_BEFORE_DELETE', ['estimate_id' => $estimate_id, 'items' => $items_to_check]);
|
|
|
|
$tempsql ="DELETE FROM {$this->item_table} WHERE estimate_id = '{$estimate_id}' AND (dealer_id IS NULL OR dealer_id = '')";
|
|
$this->logHistory($wr_id, null, 'DELETE_QUERY_EXECUTING', ['query' => $tempsql]);
|
|
|
|
$affected_rows = sql_query($tempsql);
|
|
|
|
$this->logHistory($wr_id, null, 'DELETE_QUERY_RESULT', [
|
|
'affected_rows' => $affected_rows,
|
|
'estimate_id' => $estimate_id
|
|
]);
|
|
|
|
$items = $post_data['estimate'] ?? [];
|
|
$this->logHistory($wr_id, null, 'saveCustomerRequest_items_count', ['count' => count($items)]);
|
|
foreach ($items as $index => $item) {
|
|
$item['no'] = $index + 1;
|
|
$item['estimate_id'] = $estimate_id;
|
|
unset($item['dealer_id']);
|
|
$this->logHistory($wr_id, null, 'saveCustomerRequest_item_data', ['item_index' => $index, 'item_data' => $item]);
|
|
$result= $this->createEstimateItem($estimate_id, $item);
|
|
$this->logHistory($wr_id, null, 'createEstimateItem_result', ['item_index' => $index, 'result' => $result]);
|
|
}
|
|
|
|
sql_query("COMMIT");
|
|
return $estimate_id;
|
|
} catch (Exception $e) {
|
|
sql_query("ROLLBACK");
|
|
error_log("saveCustomerRequest 오류: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* wr_id를 기반으로 견적 마스터 데이터와 아이템 데이터를 로드합니다.
|
|
* @param int $wr_id 게시물 ID
|
|
* @return array|null 견적 데이터 (마스터 + 아이템) 또는 null
|
|
*/
|
|
public function loadEstimateDataByWrId($wr_id)
|
|
{
|
|
$estimate_master = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate_master) {
|
|
return null;
|
|
}
|
|
|
|
$estimate_items = sql_query("SELECT * FROM {$this->item_table} WHERE estimate_id = '{$estimate_master['id']}' ORDER BY no ASC");
|
|
$items_array = [];
|
|
while ($row = sql_fetch_array($estimate_items)) {
|
|
$items_array[] = $row;
|
|
}
|
|
|
|
$estimate_master['items'] = $items_array;
|
|
return $estimate_master;
|
|
}
|
|
// ==============================================================================
|
|
// 🔥 새로 추가되는 메서드들 (기존 코드에 영향 없음)
|
|
// ==============================================================================
|
|
/**
|
|
* 답변 글의 원본 글 ID를 찾습니다.
|
|
* @param int $current_wr_id 현재 답변 글 ID
|
|
* @return int|null 원본 글 ID 또는 null
|
|
*/
|
|
private function findOriginalWrId($current_wr_id)
|
|
{
|
|
global $g5;
|
|
|
|
// write 테이블명 (order 게시판)
|
|
$write_table = $g5['write_prefix'] . 'order';
|
|
|
|
// 1. 현재 글 정보 조회
|
|
$current_post = sql_fetch("SELECT wr_num, wr_reply FROM {$write_table} WHERE wr_id = '{$current_wr_id}'");
|
|
if (!$current_post) {
|
|
return null;
|
|
}
|
|
|
|
// 2. 🔥 wr_reply가 빈 값이면 이미 원본 글
|
|
if (empty($current_post['wr_reply'])) {
|
|
return (int)$current_wr_id;
|
|
}
|
|
|
|
// 3. 🔥 wr_reply가 있으면 답변 글이므로, 같은 wr_num에서 wr_reply가 빈 원본 글을 찾기
|
|
$original_post = sql_fetch("SELECT wr_id FROM {$write_table} WHERE wr_num = '{$current_post['wr_num']}' AND wr_reply = ''");
|
|
if ($original_post) {
|
|
return (int)$original_post['wr_id'];
|
|
}
|
|
|
|
// 4. 찾지 못한 경우 null 반환
|
|
return null;
|
|
}
|
|
/**
|
|
* 대리점의 견적 제안을 저장합니다.
|
|
* @param int $current_wr_id 현재 답변 글의 ID
|
|
* @param array $post_data 폼 데이터
|
|
* @return int|bool estimate_id 또는 실패 시 false
|
|
*/
|
|
public function saveAgentProposal($current_wr_id, $post_data)
|
|
{
|
|
$_POST['from_save_estimate'] = true;
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["1",$current_wr_id,$post_data]);
|
|
|
|
|
|
try {
|
|
sql_query("START TRANSACTION");
|
|
// 1. 원본 글 ID 찾기
|
|
$original_wr_id = $this->findOriginalWrId($current_wr_id);
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["2",$current_wr_id,$original_wr_id]);
|
|
if (!$original_wr_id) {
|
|
throw new Exception('원본 글을 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 원본 견적서 정보 조회 (고객의 기본 정보 복사용)
|
|
$original_estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$original_wr_id}' AND is_deleted = 0");
|
|
if (!$original_estimate) {
|
|
throw new Exception('원본 견적서를 찾을 수 없습니다.');
|
|
}
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["3",$current_wr_id,$this->is_admin,$original_estimate]);
|
|
$dealer_id = $this->member['mb_id'] ?? '';
|
|
|
|
// 🔥 관리자(Level 10) 또는 전문가(Level 9)인 경우, 글 작성자가 본인일 수 있음
|
|
// 기존 코드: if($this->is_admin) { ... }
|
|
// 여기서는 $dealer_id가 현재 로그인한 멤버의 ID임.
|
|
// 관리자가 대리점을 대신해서 올리는 경우 등은 별도 로직이 필요할 수 있으나,
|
|
// 요구사항은 "전문가나 관리자가 등록을 해야되" 이므로 본인 ID로 등록하면 됨.
|
|
|
|
// 기존 코드 유지 (관리자일 때 created_by 가져오는 부분은 상황에 따라 다를 수 있음)
|
|
if($this->is_admin) {
|
|
$tmpuser = sql_fetch("SELECT created_by FROM {$this->estimate_table} WHERE wr_id = '{$current_wr_id}' AND is_deleted = 0") ?? '';
|
|
if ($tmpuser) {
|
|
// $dealer_id = $tmpuser['created_by'];
|
|
// 주의: 위 로직은 이미 생성된 견적이 있을 때 작성자를 가져오는 것임.
|
|
// 신규 작성 시에는 현재 로그인한 관리자의 ID를 사용해야 함.
|
|
// 따라서 $dealer_id는 기본적으로 $member['mb_id']를 사용하고,
|
|
// 수정 시에는 기존 작성자를 유지하는 방식이 안전함.
|
|
}
|
|
}
|
|
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["3-1",$dealer_id]);
|
|
if (empty($dealer_id)) {
|
|
throw new Exception('대리점 정보가 없습니다.');
|
|
}
|
|
|
|
// 3. 🔥 [핵심] 대리점용 견적 마스터 정보 생성
|
|
// 기존 견적이 있는지 확인 (대리점이 이미 제안한 경우)
|
|
// $sql ="SELECT id FROM {$this->estimate_table} WHERE wr_id = '{$current_wr_id}' AND is_deleted = 0";
|
|
$existing_agent_estimate = sql_fetch("SELECT id FROM {$this->estimate_table} WHERE wr_id = '{$current_wr_id}' AND is_deleted = 0");
|
|
|
|
// 1. 최종 금액을 폼 데이터에서 직접 가져옵니다.
|
|
$final_amount = (int)($post_data['estimate']['final_amount'] ?? 0);
|
|
|
|
// 만약 final_amount가 없으면 item들의 합계로 계산
|
|
if ($final_amount == 0 && isset($post_data['estimate']) && is_array($post_data['estimate'])) {
|
|
foreach ($post_data['estimate'] as $item) {
|
|
if(is_array($item)) {
|
|
$final_amount += (int)($item['amount'] ?? 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["4",$current_wr_id,$sql,$existing_agent_estimate]);
|
|
if ($existing_agent_estimate) {
|
|
// 기존 대리점 견적 수정
|
|
$estimate_id = $existing_agent_estimate['id'];
|
|
// 대리점 견적 마스터 정보 업데이트 (필요한 경우)
|
|
$update_data = [
|
|
'status' => '견적제안',
|
|
'commission_fee' => $final_amount, // 🔥 commission_fee 필드를 최종 금액 저장용으로 사용
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
'updated_by' => $dealer_id,
|
|
];
|
|
$this->update($this->estimate_table, $estimate_id, $update_data);
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["4-2",$this->update($this->estimate_table, $estimate_id, $update_data)]);
|
|
|
|
} else {
|
|
// 🔥 새로운 대리점 견적 마스터 생성
|
|
$agent_estimate_data = [
|
|
'wr_id' => $current_wr_id, // 🔥 답변 글 ID로 설정
|
|
'status' => '견적제안',
|
|
|
|
// 고객의 기본 정보 복사
|
|
'zip_code' => $original_estimate['zip_code'],
|
|
'address1' => $original_estimate['address1'],
|
|
'address2' => $original_estimate['address2'],
|
|
'address3' => $original_estimate['address3'],
|
|
'house_type' => $original_estimate['house_type'],
|
|
'house_size' => $original_estimate['house_size'],
|
|
'material' => $original_estimate['material'],
|
|
'color' => $original_estimate['color'],
|
|
'glass_thickness' => $original_estimate['glass_thickness'],
|
|
'install' => $original_estimate['install'],
|
|
'commission_fee' => $final_amount, // 🔥 commission_fee 필드를 최종 금액 저장용으로 사용
|
|
// 대리점 정보
|
|
'created_by' => $dealer_id,
|
|
'updated_by' => $dealer_id,
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
$estimate_id = $this->create($this->estimate_table, $agent_estimate_data);
|
|
if (!$estimate_id) {
|
|
throw new Exception('대리점 견적 마스터 생성 실패');
|
|
}
|
|
|
|
error_log("대리점 견적 마스터 생성 완료 - estimate_id: {$estimate_id}, current_wr_id: {$current_wr_id}, dealer_id: {$dealer_id}");
|
|
}
|
|
|
|
|
|
// 💡 [추가] 입찰(Bidding) 정보도 생성/업데이트해야 함
|
|
// 견적(Estimate) 테이블은 상세 내용을 담고, 입찰(Bidding) 테이블은 금액과 대리점 정보를 담아 입찰 프로세스를 관리함.
|
|
// EstimateManager::createBiddingWithItems 메서드 로직을 참조하여 여기서도 Bidding 정보를 생성해야 함.
|
|
|
|
// 원본 글 ID에 대한 입찰 정보를 생성해야 함.
|
|
// $original_wr_id -> $original_estimate['id']
|
|
|
|
$bidding_data = [
|
|
'total_amount' => $final_amount,
|
|
'message' => $post_data['wr_content'] ?? '', // 게시판 본문 내용
|
|
];
|
|
|
|
// createBiddingWithItems는 estimate_id(고객요청)를 기준으로 동작함.
|
|
// 하지만 여기서는 saveAgentProposal이 대리점용 estimate 레코드를 별도로 생성하고 있음($estimate_id).
|
|
// 시스템 구조가 다소 혼재되어 있어 보임.
|
|
// 1. 고객 estimate -> 대리점 bidding (일반적인 구조)
|
|
// 2. 고객 estimate -> 대리점 estimate (상세 견적 제안용)
|
|
|
|
// 요구사항은 "대리점처럼 될수 있게" 이므로, 입찰 정보도 생성해주는 것이 좋음.
|
|
// createBiddingWithItems($original_estimate['id'], $bidding_data) 호출 고려.
|
|
|
|
$this->createBiddingWithItems($original_estimate['id'], $bidding_data);
|
|
|
|
|
|
// 4. 기존 대리점 견적 아이템 삭제 (수정 시)
|
|
// 여기서는 위에서 생성한 대리점용 estimate_id에 대한 아이템을 관리함
|
|
$sql1 ="DELETE FROM {$this->item_table} WHERE estimate_id = '{$estimate_id}' AND dealer_id = '{$dealer_id}'";
|
|
$sql2 ="DELETE FROM {$this->item_table} WHERE estimate_id = '{$estimate_id}' AND (dealer_id IS NULL OR dealer_id = '')";
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["4-4",$sql1,$sql2]);
|
|
sql_query("DELETE FROM {$this->item_table} WHERE estimate_id = '{$estimate_id}' AND dealer_id = '{$dealer_id}'");
|
|
sql_query("DELETE FROM {$this->item_table} WHERE estimate_id = '{$estimate_id}' AND (dealer_id IS NULL OR dealer_id = '')");
|
|
|
|
// 5. 🔥 대리점 견적 아이템 저장
|
|
$items = $post_data['estimate'] ?? [];
|
|
|
|
foreach ($items as $index => $item) {
|
|
if(!is_array($item))continue;;
|
|
// $this->logHistory($current_wr_id, null, 'saveAgentProposal', ["4-5",$item]);
|
|
|
|
$item_data = [
|
|
'estimate_id' => $estimate_id, // 🔥 대리점의 estimate_id 사용
|
|
'dealer_id' => $dealer_id,
|
|
'no' => $index + 1,
|
|
|
|
// 고객이 입력한 기본 정보
|
|
'location' => trim($item['location'] ?? ''),
|
|
'spec_width' => trim($item['spec_width'] ?? ''),
|
|
'spec_height' => trim($item['spec_height'] ?? ''),
|
|
'window_main_type' => trim($item['window_main_type'] ?? ''),
|
|
'windowType' => trim($item['windowType'] ?? ''),
|
|
'windowRatio' => trim($item['windowRatio'] ?? ''),
|
|
'glass_color' => trim($item['glass_color'] ?? ''),
|
|
'handle' => trim($item['handle'] ?? ''),
|
|
'replacePart' => trim($item['replacePart'] ?? ''),
|
|
'extra_1' => trim($item['extra_1'] ?? ''),
|
|
|
|
// 🔥 대리점이 입력한 견적 정보
|
|
'product' => trim($item['product'] ?? ''),
|
|
'qty' => (int)($item['qty'] ?? 1),
|
|
'price' => (int)($item['price'] ?? 0),
|
|
'amount' => (int)($item['amount'] ?? 0),
|
|
'brand' => trim($item['brand'] ?? ''),
|
|
|
|
'created_by' => $dealer_id,
|
|
'updated_by' => $dealer_id,
|
|
// 'dealer_id'=>$dealer_id,
|
|
];
|
|
|
|
$result = $this->create($this->item_table, $item_data);
|
|
if (!$result) {
|
|
throw new Exception("견적 아이템 저장 실패 - 인덱스: {$index}");
|
|
}
|
|
}
|
|
|
|
$this->logHistory($estimate_id, null, 'saveAgentProposal', [
|
|
'current_wr_id' => $current_wr_id,
|
|
'original_wr_id' => $original_wr_id,
|
|
'dealer_id' => $dealer_id,
|
|
'items_count' => count($items)
|
|
]);
|
|
|
|
sql_query("COMMIT");
|
|
return $estimate_id;
|
|
|
|
} catch (Exception $e) {
|
|
sql_query("ROLLBACK");
|
|
error_log('saveAgentProposal Error: ' . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
/**
|
|
* 견적서의 상태를 변경합니다.
|
|
* @param int $id estimate_id
|
|
* @param string $new_status 새로운 상태
|
|
* @param array $extra 추가 데이터
|
|
* @return bool
|
|
*/
|
|
public function updateEstimateStatus($id, $new_status, $extra = [])
|
|
{
|
|
$old = sql_fetch("SELECT status FROM {$this->estimate_table} WHERE id = '{$id}'");
|
|
if (!$old)
|
|
return false;
|
|
|
|
$set_clauses = [
|
|
'status' => $new_status,
|
|
'updated_by' => $this->member['mb_id'] ?? '',
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
if ($new_status === '작성완료') {
|
|
$deadline = date('Y-m-d H:i:s', strtotime('+24 hours'));
|
|
$set_clauses['bidding_deadline'] = $deadline;
|
|
}
|
|
|
|
$result = $this->update($this->estimate_table, $id, $set_clauses);
|
|
if ($result) {
|
|
$this->logHistory($id, null, 'status_change', ['old' => $old['status'], 'new' => $new_status, 'extra' => $extra]);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
// ==============================================================================
|
|
// 헬퍼 메서드들
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* 주문 설정 조회
|
|
*/
|
|
private function getOrderConfig()
|
|
{
|
|
$config = [];
|
|
$result = sql_query("SELECT config_key, config_value FROM order_config");
|
|
while ($row = sql_fetch_array($result)) {
|
|
$config[$row['config_key']] = $row['config_value'];
|
|
}
|
|
return $config;
|
|
}
|
|
|
|
private function createEstimateItem($estimate_id, $item)
|
|
{
|
|
// $resylt = $this->create($this->item_table, $item);
|
|
// $this->write_debug_log("[createEstimateItem] $resylt: {$resylt}");
|
|
return $this->create($this->item_table, $item);
|
|
}
|
|
|
|
private function create($table, $data)
|
|
{
|
|
$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 `{$table}` ({$fields_str}) VALUES ({$values_str})";
|
|
|
|
// $this->write_debug_log("[create] sql: {$sql}");
|
|
$result = sql_query($sql);
|
|
// $this->write_debug_log("[create] result: {$result}");
|
|
|
|
return $result ? sql_insert_id() : false;
|
|
}
|
|
|
|
private function update($table, $id, $data)
|
|
{
|
|
if (is_array($data)) {
|
|
$set_clauses = [];
|
|
foreach ($data as $key => $value) {
|
|
$set_clauses[] = "`{$key}` = '" . sql_real_escape_string($value) . "'";
|
|
}
|
|
$set_str = implode(', ', $set_clauses);
|
|
} else {
|
|
$set_str = $data;
|
|
}
|
|
$sql = "UPDATE `{$table}` SET {$set_str} WHERE id = '{$id}'";
|
|
return sql_query($sql);
|
|
}
|
|
|
|
private function logHistory($estimate_id, $item_id, $action, $details)
|
|
{
|
|
$history_data = [
|
|
'estimate_id' => $estimate_id,
|
|
'item_id' => $item_id,
|
|
'action' => $action,
|
|
'change_details' => json_encode($details, JSON_UNESCAPED_UNICODE),
|
|
'changed_by' => $this->member['mb_id'] ?? '',
|
|
'changed_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->create($this->history_table, $history_data);
|
|
}
|
|
|
|
/**
|
|
* 대리점 선택 알림 발송
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $dealer_id 대리점 ID
|
|
* @param array $bid_info 입찰 정보
|
|
*/
|
|
private function sendDealerSelectedNotification($wr_id, $dealer_id, $bid_info)
|
|
{
|
|
try {
|
|
// DealerNotification 클래스 로드
|
|
if (file_exists(G5_PATH . '/adm/order_manage/dealer_notification.php')) {
|
|
require_once G5_PATH . '/adm/order_manage/dealer_notification.php';
|
|
$dealer_notification = new DealerNotification();
|
|
|
|
$result = $dealer_notification->sendBidSelectedNotification($wr_id, $dealer_id, $bid_info);
|
|
|
|
if (!$result['success']) {
|
|
error_log("Dealer notification failed: " . $result['message']);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::sendDealerSelectedNotification Error: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// ==============================================================================
|
|
// 전문가 방문 시스템 (Task 6.1 - Requirements 5.1, 5.2, 5.3, 13.1, 13.2, 13.3)
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* 전문가 방문 요청
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function requestExpertVisit($wr_id, $user_role = 'customer')
|
|
{
|
|
try {
|
|
// 1. 견적서 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 권한 확인 (고객 본인 또는 관리자만)
|
|
if ($user_role === 'customer') {
|
|
$customer_id = $this->member['mb_id'] ?? '';
|
|
if ($estimate['created_by'] !== $customer_id) {
|
|
throw new Exception('해당 견적에 대한 권한이 없습니다.');
|
|
}
|
|
}
|
|
|
|
// 3. 이미 전문가 방문이 요청되었는지 확인
|
|
if ($estimate['temp_1'] === 'Y') {
|
|
throw new Exception('이미 전문가 방문이 요청되었습니다.');
|
|
}
|
|
|
|
// 4. 전문가 방문 비용 계산
|
|
$config = $this->getOrderConfig();
|
|
$visit_fee = (int) ($config['expert_visit_fee'] ?? 50000);
|
|
|
|
// 5. 견적서 업데이트 (temp 컬럼 활용)
|
|
$update_data = [
|
|
'temp_1' => 'Y', // 전문가 방문 요청 여부
|
|
'temp_2' => 'requested', // 전문가 방문 상태
|
|
'temp_3' => $visit_fee, // 전문가 방문 비용
|
|
'temp_4' => null, // 전문가 방문 일정 (추후 설정)
|
|
'temp_5' => '전문가 방문 요청됨', // 전문가 방문 메모
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$update_result = $this->update($this->estimate_table, $estimate['id'], $update_data);
|
|
if (!$update_result) {
|
|
throw new Exception('전문가 방문 요청 저장에 실패했습니다.');
|
|
}
|
|
|
|
// 6. 상태 변경 (전문가방문요청)
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$wr_id,
|
|
'전문가방문요청',
|
|
$user_id,
|
|
$user_role,
|
|
"전문가 방문 요청 - 비용: {$visit_fee}원"
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
// 7. 이력 기록
|
|
$this->logHistory($estimate['id'], null, 'expert_visit_requested', [
|
|
'visit_fee' => $visit_fee,
|
|
'requested_by' => $user_id,
|
|
'request_date' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
// 8. 고객 알림 발송 (결제 안내)
|
|
$this->sendExpertVisitNotification($wr_id, 'request', [
|
|
'visit_fee' => $visit_fee,
|
|
'account_info' => $config['payment_account_info'] ?? ''
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'visit_fee' => $visit_fee,
|
|
'status' => 'requested',
|
|
'account_info' => $config['payment_account_info'] ?? ''
|
|
],
|
|
'message' => '전문가 방문이 요청되었습니다. 방문 비용을 입금해주세요.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::requestExpertVisit Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전문가 방문 정보 조회
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @return array|false 전문가 방문 정보 또는 실패 시 false
|
|
*/
|
|
public function getExpertVisitInfo($wr_id)
|
|
{
|
|
try {
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
return false;
|
|
}
|
|
|
|
$visit_info = [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'is_requested' => $estimate['temp_1'] === 'Y',
|
|
'status' => $estimate['temp_2'] ?? 'not_requested',
|
|
'visit_fee' => (int) ($estimate['temp_3'] ?? 0),
|
|
'visit_datetime' => $estimate['temp_4'] ?? null,
|
|
'visit_notes' => $estimate['temp_5'] ?? '',
|
|
'expert_id' => $estimate['extra_4'] ?? '',
|
|
'completed_date' => $estimate['extra_5'] ?? null
|
|
];
|
|
|
|
// 전문가 정보 추가
|
|
if ($visit_info['expert_id']) {
|
|
$expert = sql_fetch("SELECT mb_name, mb_hp FROM g5_member WHERE mb_id = '{$visit_info['expert_id']}'");
|
|
if ($expert) {
|
|
$visit_info['expert_name'] = $expert['mb_name'];
|
|
$visit_info['expert_phone'] = $expert['mb_hp'];
|
|
}
|
|
}
|
|
|
|
return $visit_info;
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getExpertVisitInfo Error: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전문가 방문 결제 확인
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function confirmExpertVisitPayment($wr_id, $user_role = 'admin')
|
|
{
|
|
try {
|
|
// 1. 견적서 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 전문가 방문 요청 상태 확인
|
|
if ($estimate['temp_1'] !== 'Y' || $estimate['temp_2'] !== 'requested') {
|
|
throw new Exception('전문가 방문 요청 상태가 아닙니다.');
|
|
}
|
|
|
|
// 3. 견적서 상태 업데이트
|
|
$update_data = [
|
|
'temp_2' => 'payment_confirmed', // 전문가 방문 상태
|
|
'temp_5' => '전문가 방문 비용 입금 확인됨',
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$update_result = $this->update($this->estimate_table, $estimate['id'], $update_data);
|
|
if (!$update_result) {
|
|
throw new Exception('결제 확인 정보 저장에 실패했습니다.');
|
|
}
|
|
|
|
// 4. 상태 변경 (전문가방문결제완료)
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$wr_id,
|
|
'전문가방문결제완료',
|
|
$user_id,
|
|
$user_role,
|
|
'전문가 방문 비용 결제 확인'
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
// 5. 이력 기록
|
|
$this->logHistory($estimate['id'], null, 'expert_visit_payment_confirmed', [
|
|
'confirmed_by' => $user_id,
|
|
'confirmed_date' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'status' => 'payment_confirmed'
|
|
],
|
|
'message' => '전문가 방문 비용 결제가 확인되었습니다.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::confirmExpertVisitPayment Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전문가 방문 일정 설정
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $visit_datetime 방문 일시 (Y-m-d H:i)
|
|
* @param string $expert_id 전문가 ID
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function scheduleExpertVisit($wr_id, $visit_datetime, $expert_id = '', $user_role = 'admin')
|
|
{
|
|
try {
|
|
// 1. 견적서 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 결제 확인 상태 체크
|
|
if ($estimate['temp_2'] !== 'payment_confirmed') {
|
|
throw new Exception('전문가 방문 비용 결제가 확인되지 않았습니다.');
|
|
}
|
|
|
|
// 3. 일시 유효성 검증
|
|
$visit_timestamp = strtotime($visit_datetime);
|
|
if (!$visit_timestamp || $visit_timestamp < time()) {
|
|
throw new Exception('유효하지 않은 방문 일시입니다.');
|
|
}
|
|
|
|
// 4. 견적서 업데이트
|
|
$update_data = [
|
|
'temp_2' => 'scheduled', // 전문가 방문 상태
|
|
'temp_4' => $visit_datetime, // 전문가 방문 일정
|
|
'temp_5' => "전문가 방문 일정: {$visit_datetime}" . ($expert_id ? " (담당: {$expert_id})" : ''),
|
|
'extra_4' => $expert_id, // 담당 전문가 ID (extra 컬럼 활용)
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$update_result = $this->update($this->estimate_table, $estimate['id'], $update_data);
|
|
if (!$update_result) {
|
|
throw new Exception('방문 일정 저장에 실패했습니다.');
|
|
}
|
|
|
|
// 5. 상태 변경 (전문가방문예정)
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$wr_id,
|
|
'전문가방문예정',
|
|
$user_id,
|
|
$user_role,
|
|
"전문가 방문 일정: {$visit_datetime}"
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
// 6. 이력 기록
|
|
$this->logHistory($estimate['id'], null, 'expert_visit_scheduled', [
|
|
'visit_datetime' => $visit_datetime,
|
|
'expert_id' => $expert_id,
|
|
'scheduled_by' => $user_id
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'visit_datetime' => $visit_datetime,
|
|
'expert_id' => $expert_id,
|
|
'status' => 'scheduled'
|
|
],
|
|
'message' => '전문가 방문 일정이 설정되었습니다.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::scheduleExpertVisit Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전문가 방문 완료 처리
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $visit_notes 방문 메모
|
|
* @param string $user_role 사용자 역할
|
|
* @return array ['success' => bool, 'data' => array, 'message' => string]
|
|
*/
|
|
public function completeExpertVisit($wr_id, $visit_notes = '', $user_role = 'admin')
|
|
{
|
|
try {
|
|
// 1. 견적서 정보 조회
|
|
$estimate = sql_fetch("SELECT * FROM {$this->estimate_table} WHERE wr_id = '{$wr_id}' AND is_deleted = 0");
|
|
if (!$estimate) {
|
|
throw new Exception('견적서를 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 2. 방문 예정 상태 확인
|
|
if ($estimate['temp_2'] !== 'scheduled') {
|
|
throw new Exception('전문가 방문 예정 상태가 아닙니다.');
|
|
}
|
|
|
|
// 3. 견적서 업데이트
|
|
$update_data = [
|
|
'temp_2' => 'completed', // 전문가 방문 상태
|
|
'temp_5' => "전문가 방문 완료: {$visit_notes}",
|
|
'extra_5' => date('Y-m-d H:i:s'), // 완료 시간 (extra 컬럼 활용)
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$update_result = $this->update($this->estimate_table, $estimate['id'], $update_data);
|
|
if (!$update_result) {
|
|
throw new Exception('방문 완료 정보 저장에 실패했습니다.');
|
|
}
|
|
|
|
// 4. 상태 변경 (전문가방문완료)
|
|
$user_id = $this->member['mb_id'] ?? 'system';
|
|
$status_result = $this->status_manager->changeStatus(
|
|
$wr_id,
|
|
'전문가방문완료',
|
|
$user_id,
|
|
$user_role,
|
|
"전문가 방문 완료: {$visit_notes}"
|
|
);
|
|
|
|
if (!$status_result['success']) {
|
|
throw new Exception('상태 변경 실패: ' . $status_result['message']);
|
|
}
|
|
|
|
// 5. 이력 기록
|
|
$this->logHistory($estimate['id'], null, 'expert_visit_completed', [
|
|
'visit_notes' => $visit_notes,
|
|
'completed_by' => $user_id,
|
|
'completed_date' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => [
|
|
'estimate_id' => $estimate['id'],
|
|
'wr_id' => $wr_id,
|
|
'visit_notes' => $visit_notes,
|
|
'completed_date' => date('Y-m-d H:i:s'),
|
|
'status' => 'completed'
|
|
],
|
|
'message' => '전문가 방문이 완료되었습니다.'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::completeExpertVisit Error: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전문가 방문 목록 조회 (관리자용)
|
|
*
|
|
* @param string $status_filter 상태 필터
|
|
* @param string $date_from 시작 날짜
|
|
* @param string $date_to 종료 날짜
|
|
* @return array 전문가 방문 목록
|
|
*/
|
|
public function getExpertVisitList($status_filter = null, $date_from = null, $date_to = null)
|
|
{
|
|
try {
|
|
$where_conditions = ["e.is_deleted = 0", "e.temp_1 = 'Y'"];
|
|
|
|
if ($status_filter) {
|
|
$where_conditions[] = "e.temp_2 = '{$status_filter}'";
|
|
}
|
|
|
|
if ($date_from) {
|
|
$where_conditions[] = "DATE(e.temp_4) >= '{$date_from}'";
|
|
}
|
|
|
|
if ($date_to) {
|
|
$where_conditions[] = "DATE(e.temp_4) <= '{$date_to}'";
|
|
}
|
|
|
|
$where_clause = implode(' AND ', $where_conditions);
|
|
|
|
$sql = "
|
|
SELECT
|
|
e.id as estimate_id,
|
|
e.wr_id,
|
|
e.temp_1 as is_requested,
|
|
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,
|
|
e.extra_5 as completed_date,
|
|
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,
|
|
em.mb_hp as expert_phone
|
|
FROM {$this->estimate_table} 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.temp_4 ASC, e.id DESC
|
|
";
|
|
|
|
$result = sql_query($sql);
|
|
$visits = [];
|
|
|
|
while ($row = sql_fetch_array($result)) {
|
|
$visits[] = $row;
|
|
}
|
|
|
|
return $visits;
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getExpertVisitList Error: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전문가 방문 알림 발송
|
|
*
|
|
* @param int $wr_id 게시물 ID
|
|
* @param string $notification_type 알림 타입
|
|
* @param array $additional_data 추가 데이터
|
|
*/
|
|
private function sendExpertVisitNotification($wr_id, $notification_type, $additional_data = [])
|
|
{
|
|
try {
|
|
// NotificationHelper 클래스 로드
|
|
if (file_exists(G5_PATH . '/adm/order_manage/classes/NotificationHelper.class.php')) {
|
|
require_once G5_PATH . '/adm/order_manage/classes/NotificationHelper.class.php';
|
|
$notification_helper = new NotificationHelper();
|
|
|
|
// 고객 정보 조회
|
|
$write = sql_fetch("SELECT * FROM g5_write_order WHERE wr_id = '{$wr_id}'");
|
|
if ($write) {
|
|
$customer = sql_fetch("SELECT * FROM g5_member WHERE mb_id = '{$write['mb_id']}'");
|
|
|
|
$notification_data = array_merge([
|
|
'customer_name' => $customer['mb_name'] ?? '고객',
|
|
'customer_email' => $customer['mb_email'] ?? '',
|
|
'customer_phone' => $customer['mb_hp'] ?? '',
|
|
'estimate_id' => $wr_id,
|
|
'estimate_subject' => $write['wr_subject']
|
|
], $additional_data);
|
|
|
|
// 알림 타입별 템플릿 키 결정
|
|
$template_key = "expert_visit_{$notification_type}";
|
|
|
|
$result = $notification_helper->sendCustomerNotification($template_key, $notification_data);
|
|
|
|
if (!$result['success']) {
|
|
error_log("Expert visit notification failed: " . $result['message']);
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::sendExpertVisitNotification Error: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// ==============================================================================
|
|
// 통계 및 리포트 시스템 (Task 7.2 - Requirements 12.1, 12.2, 12.3, 12.4)
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* 견적 요청 현황 통계
|
|
*
|
|
* @param string $date_from 시작 날짜
|
|
* @param string $date_to 종료 날짜
|
|
* @return array 통계 데이터
|
|
*/
|
|
public function getEstimateStatistics($date_from = null, $date_to = null)
|
|
{
|
|
try {
|
|
$where_conditions = ["e.is_deleted = 0"];
|
|
$trend_where_conditions = ["e.is_deleted = 0"]; // 월별 트렌드용 WHERE 조건
|
|
|
|
if ($date_from) {
|
|
$where_conditions[] = "DATE(e.created_at) >= '{$date_from}'";
|
|
$trend_where_conditions[] = "DATE(e.created_at) >= '{$date_from}'";
|
|
}
|
|
|
|
if ($date_to) {
|
|
$where_conditions[] = "DATE(e.created_at) <= '{$date_to}'";
|
|
$trend_where_conditions[] = "DATE(e.created_at) <= '{$date_to}'";
|
|
}
|
|
|
|
$where_clause = implode(' AND ', $where_conditions);
|
|
$trend_where_clause = implode(' AND ', $trend_where_conditions); // 월별 트렌드용 WHERE 절
|
|
|
|
// 1. 전체 견적 현황
|
|
$total_sql = "
|
|
SELECT
|
|
COUNT(*) as total_estimates,
|
|
COUNT(CASE WHEN e.status = '작성완료' THEN 1 END) as completed_estimates,
|
|
COUNT(CASE WHEN e.selected_bid_id IS NOT NULL THEN 1 END) as selected_estimates,
|
|
COUNT(CASE WHEN e.temp_1 = 'Y' THEN 1 END) as expert_visit_requests
|
|
FROM {$this->estimate_table} e
|
|
WHERE {$where_clause}
|
|
";
|
|
|
|
$total_stats = sql_fetch($total_sql);
|
|
|
|
// 2. 상태별 분포
|
|
$status_sql = "
|
|
SELECT
|
|
w.wr_1 as status,
|
|
COUNT(*) as count
|
|
FROM {$this->estimate_table} e
|
|
LEFT JOIN g5_write_order w ON e.wr_id = w.wr_id
|
|
WHERE {$where_clause}
|
|
GROUP BY w.wr_1
|
|
ORDER BY count DESC
|
|
";
|
|
|
|
$status_result = sql_query($status_sql);
|
|
$status_distribution = [];
|
|
while ($row = sql_fetch_array($status_result)) {
|
|
$status_distribution[] = [
|
|
'status' => $row['status'] ?: '미분류',
|
|
'count' => (int) $row['count']
|
|
];
|
|
}
|
|
|
|
// 3. 월별 트렌드 (사용자 지정 기간)
|
|
$trend_sql = "
|
|
SELECT
|
|
DATE_FORMAT(e.created_at, '%Y-%m') as month,
|
|
COUNT(*) as total_count,
|
|
COUNT(CASE WHEN e.selected_bid_id IS NOT NULL THEN 1 END) as selected_count
|
|
FROM {$this->estimate_table} e
|
|
WHERE {$trend_where_clause}
|
|
GROUP BY DATE_FORMAT(e.created_at, '%Y-%m')
|
|
ORDER BY month ASC
|
|
";
|
|
|
|
$trend_result = sql_query($trend_sql);
|
|
$monthly_trend = [];
|
|
while ($row = sql_fetch_array($trend_result)) {
|
|
$monthly_trend[] = [
|
|
'month' => $row['month'],
|
|
'total_count' => (int) $row['total_count'],
|
|
'selected_count' => (int) $row['selected_count'],
|
|
'conversion_rate' => $row['total_count'] > 0 ? round(($row['selected_count'] / $row['total_count']) * 100, 2) : 0
|
|
];
|
|
}
|
|
|
|
// 4. 전환율 계산
|
|
$conversion_rate = $total_stats['total_estimates'] > 0
|
|
? round(($total_stats['selected_estimates'] / $total_stats['total_estimates']) * 100, 2)
|
|
: 0;
|
|
|
|
return [
|
|
'total_statistics' => array_merge($total_stats, ['conversion_rate' => $conversion_rate]),
|
|
'status_distribution' => $status_distribution,
|
|
'monthly_trend' => $monthly_trend, // 이미 ASC로 정렬했으므로 array_reverse 제거
|
|
'date_range' => [
|
|
'from' => $date_from,
|
|
'to' => $date_to
|
|
]
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getEstimateStatistics Error: " . $e->getMessage());
|
|
return [
|
|
'total_statistics' => [],
|
|
'status_distribution' => [],
|
|
'monthly_trend' => [],
|
|
'date_range' => []
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 대리점별 성과 분석
|
|
*
|
|
* @param string $date_from 시작 날짜
|
|
* @param string $date_to 종료 날짜
|
|
* @return array 대리점 성과 데이터
|
|
*/
|
|
public function getDealerPerformance($date_from = null, $date_to = null)
|
|
{
|
|
try {
|
|
$where_conditions = ["b.created_at IS NOT NULL"];
|
|
|
|
if ($date_from) {
|
|
$where_conditions[] = "DATE(b.created_at) >= '{$date_from}'";
|
|
}
|
|
|
|
if ($date_to) {
|
|
$where_conditions[] = "DATE(b.created_at) <= '{$date_to}'";
|
|
}
|
|
|
|
$where_clause = implode(' AND ', $where_conditions);
|
|
|
|
$sql = "
|
|
SELECT
|
|
b.dealer_id,
|
|
m.mb_name as dealer_name,
|
|
m.mb_level as dealer_level,
|
|
COUNT(*) as total_bids,
|
|
COUNT(CASE WHEN b.status = 'selected' THEN 1 END) as selected_bids,
|
|
AVG(b.total_amount) as avg_bid_amount,
|
|
SUM(CASE WHEN b.status = 'selected' THEN b.total_amount ELSE 0 END) as total_revenue,
|
|
MIN(b.created_at) as first_bid_date,
|
|
MAX(b.created_at) as last_bid_date
|
|
FROM {$this->bidding_table} b
|
|
LEFT JOIN g5_member m ON b.dealer_id = m.mb_id
|
|
WHERE {$where_clause}
|
|
GROUP BY b.dealer_id, m.mb_name, m.mb_level
|
|
ORDER BY selected_bids DESC, total_bids DESC
|
|
";
|
|
|
|
$result = sql_query($sql);
|
|
$dealer_performance = [];
|
|
|
|
while ($row = sql_fetch_array($result)) {
|
|
$success_rate = $row['total_bids'] > 0
|
|
? round(($row['selected_bids'] / $row['total_bids']) * 100, 2)
|
|
: 0;
|
|
|
|
$dealer_performance[] = [
|
|
'dealer_id' => $row['dealer_id'],
|
|
'dealer_name' => $row['dealer_name'] ?: $row['dealer_id'],
|
|
'dealer_level' => (int) $row['dealer_level'],
|
|
'total_bids' => (int) $row['total_bids'],
|
|
'selected_bids' => (int) $row['selected_bids'],
|
|
'success_rate' => $success_rate,
|
|
'avg_bid_amount' => (int) $row['avg_bid_amount'],
|
|
'total_revenue' => (int) $row['total_revenue'],
|
|
'first_bid_date' => $row['first_bid_date'],
|
|
'last_bid_date' => $row['last_bid_date']
|
|
];
|
|
}
|
|
|
|
return $dealer_performance;
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getDealerPerformance Error: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 매출 및 결제 현황
|
|
*
|
|
* @param string $date_from 시작 날짜
|
|
* @param string $date_to 종료 날짜
|
|
* @return array 매출 데이터
|
|
*/
|
|
public function getRevenueStatistics($date_from = null, $date_to = null)
|
|
{
|
|
try {
|
|
$where_conditions = ["e.is_deleted = 0", "e.selected_bid_id IS NOT NULL"];
|
|
|
|
if ($date_from) {
|
|
$where_conditions[] = "DATE(e.created_at) >= '{$date_from}'";
|
|
}
|
|
|
|
if ($date_to) {
|
|
$where_conditions[] = "DATE(e.created_at) <= '{$date_to}'";
|
|
}
|
|
|
|
$where_clause = implode(' AND ', $where_conditions);
|
|
|
|
// 1. 전체 매출 현황
|
|
$revenue_sql = "
|
|
SELECT
|
|
COUNT(*) as total_orders,
|
|
SUM(b.total_amount) as total_revenue,
|
|
AVG(b.total_amount) as avg_order_value,
|
|
COUNT(CASE WHEN w.wr_1 IN ('입금확인', '중도금입금완료', '잔금입금완료', '시공완료') THEN 1 END) as paid_orders,
|
|
SUM(CASE WHEN w.wr_1 IN ('입금확인', '중도금입금완료', '잔금입금완료', '시공완료') THEN b.total_amount ELSE 0 END) as paid_revenue
|
|
FROM {$this->estimate_table} e
|
|
LEFT JOIN {$this->bidding_table} b ON e.selected_bid_id = b.id
|
|
LEFT JOIN g5_write_order w ON e.wr_id = w.wr_id
|
|
WHERE {$where_clause}
|
|
";
|
|
|
|
$revenue_stats = sql_fetch($revenue_sql);
|
|
|
|
// 2. 결제 단계별 현황
|
|
$payment_sql = "
|
|
SELECT
|
|
w.wr_1 as payment_status,
|
|
COUNT(*) as count,
|
|
SUM(b.total_amount) as revenue
|
|
FROM {$this->estimate_table} e
|
|
LEFT JOIN {$this->bidding_table} b ON e.selected_bid_id = b.id
|
|
LEFT JOIN g5_write_order w ON e.wr_id = w.wr_id
|
|
WHERE {$where_clause}
|
|
AND w.wr_1 IN ('견적채택', '입금예정', '입금확인', '중도금입금예정', '중도금입금완료', '잔금입금예정', '잔금입금완료', '시공완료')
|
|
GROUP BY w.wr_1
|
|
ORDER BY
|
|
CASE w.wr_1
|
|
WHEN '견적채택' THEN 1
|
|
WHEN '입금예정' THEN 2
|
|
WHEN '입금확인' THEN 3
|
|
WHEN '중도금입금예정' THEN 4
|
|
WHEN '중도금입금완료' THEN 5
|
|
WHEN '잔금입금예정' THEN 6
|
|
WHEN '잔금입금완료' THEN 7
|
|
WHEN '시공완료' THEN 8
|
|
ELSE 9
|
|
END
|
|
";
|
|
|
|
$payment_result = sql_query($payment_sql);
|
|
$payment_breakdown = [];
|
|
while ($row = sql_fetch_array($payment_result)) {
|
|
$payment_breakdown[] = [
|
|
'status' => $row['payment_status'],
|
|
'count' => (int) $row['count'],
|
|
'revenue' => (int) $row['revenue']
|
|
];
|
|
}
|
|
|
|
// 3. 월별 매출 트렌드
|
|
$monthly_revenue_sql = "
|
|
SELECT
|
|
DATE_FORMAT(e.created_at, '%Y-%m') as month,
|
|
COUNT(*) as order_count,
|
|
SUM(b.total_amount) as monthly_revenue,
|
|
AVG(b.total_amount) as avg_monthly_order
|
|
FROM {$this->estimate_table} e
|
|
LEFT JOIN {$this->bidding_table} b ON e.selected_bid_id = b.id
|
|
WHERE e.is_deleted = 0
|
|
AND e.selected_bid_id IS NOT NULL
|
|
AND e.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
|
GROUP BY DATE_FORMAT(e.created_at, '%Y-%m')
|
|
ORDER BY month DESC
|
|
LIMIT 12
|
|
";
|
|
|
|
$monthly_result = sql_query($monthly_revenue_sql);
|
|
$monthly_revenue = [];
|
|
while ($row = sql_fetch_array($monthly_result)) {
|
|
$monthly_revenue[] = [
|
|
'month' => $row['month'],
|
|
'order_count' => (int) $row['order_count'],
|
|
'revenue' => (int) $row['monthly_revenue'],
|
|
'avg_order_value' => (int) $row['avg_monthly_order']
|
|
];
|
|
}
|
|
|
|
return [
|
|
'total_revenue' => $revenue_stats,
|
|
'payment_breakdown' => $payment_breakdown,
|
|
'monthly_revenue' => array_reverse($monthly_revenue),
|
|
'date_range' => [
|
|
'from' => $date_from,
|
|
'to' => $date_to
|
|
]
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getRevenueStatistics Error: " . $e->getMessage());
|
|
return [
|
|
'total_revenue' => [],
|
|
'payment_breakdown' => [],
|
|
'monthly_revenue' => [],
|
|
'date_range' => []
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전문가 방문 통계
|
|
*
|
|
* @param string $date_from 시작 날짜
|
|
* @param string $date_to 종료 날짜
|
|
* @return array 전문가 방문 통계
|
|
*/
|
|
public function getExpertVisitStatistics($date_from = null, $date_to = null)
|
|
{
|
|
try {
|
|
$where_conditions = ["e.is_deleted = 0", "e.temp_1 = 'Y'"];
|
|
|
|
if ($date_from) {
|
|
$where_conditions[] = "DATE(e.created_at) >= '{$date_from}'";
|
|
}
|
|
|
|
if ($date_to) {
|
|
$where_conditions[] = "DATE(e.created_at) <= '{$date_to}'";
|
|
}
|
|
|
|
$where_clause = implode(' AND ', $where_conditions);
|
|
|
|
// 1. 전문가 방문 현황
|
|
$visit_sql = "
|
|
SELECT
|
|
COUNT(*) as total_requests,
|
|
COUNT(CASE WHEN e.temp_2 = 'payment_confirmed' THEN 1 END) as paid_requests,
|
|
COUNT(CASE WHEN e.temp_2 = 'scheduled' THEN 1 END) as scheduled_visits,
|
|
COUNT(CASE WHEN e.temp_2 = 'completed' THEN 1 END) as completed_visits,
|
|
SUM(CASE WHEN e.temp_3 > 0 THEN e.temp_3 ELSE 0 END) as total_visit_revenue,
|
|
AVG(CASE WHEN e.temp_3 > 0 THEN e.temp_3 ELSE NULL END) as avg_visit_fee
|
|
FROM {$this->estimate_table} e
|
|
WHERE {$where_clause}
|
|
";
|
|
|
|
$visit_stats = sql_fetch($visit_sql);
|
|
|
|
// 2. 전문가별 방문 현황
|
|
$expert_sql = "
|
|
SELECT
|
|
e.extra_4 as expert_id,
|
|
m.mb_name as expert_name,
|
|
COUNT(*) as assigned_visits,
|
|
COUNT(CASE WHEN e.temp_2 = 'completed' THEN 1 END) as completed_visits
|
|
FROM {$this->estimate_table} e
|
|
LEFT JOIN g5_member m ON e.extra_4 = m.mb_id
|
|
WHERE {$where_clause}
|
|
AND e.extra_4 IS NOT NULL AND e.extra_4 != ''
|
|
GROUP BY e.extra_4, m.mb_name
|
|
ORDER BY completed_visits DESC, assigned_visits DESC
|
|
";
|
|
|
|
$expert_result = sql_query($expert_sql);
|
|
$expert_performance = [];
|
|
while ($row = sql_fetch_array($expert_result)) {
|
|
$completion_rate = $row['assigned_visits'] > 0
|
|
? round(($row['completed_visits'] / $row['assigned_visits']) * 100, 2)
|
|
: 0;
|
|
|
|
$expert_performance[] = [
|
|
'expert_id' => $row['expert_id'],
|
|
'expert_name' => $row['expert_name'] ?: $row['expert_id'],
|
|
'assigned_visits' => (int) $row['assigned_visits'],
|
|
'completed_visits' => (int) $row['completed_visits'],
|
|
'completion_rate' => $completion_rate
|
|
];
|
|
}
|
|
|
|
return [
|
|
'visit_statistics' => $visit_stats,
|
|
'expert_performance' => $expert_performance,
|
|
'date_range' => [
|
|
'from' => $date_from,
|
|
'to' => $date_to
|
|
]
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::getExpertVisitStatistics Error: " . $e->getMessage());
|
|
return [
|
|
'visit_statistics' => [],
|
|
'expert_performance' => [],
|
|
'date_range' => []
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 통계 데이터 CSV 내보내기
|
|
*
|
|
* @param string $report_type 리포트 타입 (estimates, dealers, revenue, expert_visits)
|
|
* @param string $date_from 시작 날짜
|
|
* @param string $date_to 종료 날짜
|
|
* @return string CSV 데이터
|
|
*/
|
|
public function exportStatisticsCSV($report_type, $date_from = null, $date_to = null)
|
|
{
|
|
try {
|
|
$csv_data = '';
|
|
$headers = [];
|
|
$rows = [];
|
|
|
|
switch ($report_type) {
|
|
case 'estimates':
|
|
$stats = $this->getEstimateStatistics($date_from, $date_to);
|
|
$headers = ['월', '총 견적수', '선택된 견적수', '전환율(%)'];
|
|
foreach ($stats['monthly_trend'] as $trend) {
|
|
$rows[] = [
|
|
$trend['month'],
|
|
$trend['total_count'],
|
|
$trend['selected_count'],
|
|
$trend['conversion_rate']
|
|
];
|
|
}
|
|
break;
|
|
|
|
case 'dealers':
|
|
$dealers = $this->getDealerPerformance($date_from, $date_to);
|
|
$headers = ['대리점ID', '대리점명', '레벨', '총 입찰수', '선택된 입찰수', '성공률(%)', '평균 입찰금액', '총 매출'];
|
|
foreach ($dealers as $dealer) {
|
|
$rows[] = [
|
|
$dealer['dealer_id'],
|
|
$dealer['dealer_name'],
|
|
$dealer['dealer_level'],
|
|
$dealer['total_bids'],
|
|
$dealer['selected_bids'],
|
|
$dealer['success_rate'],
|
|
number_format($dealer['avg_bid_amount']),
|
|
number_format($dealer['total_revenue'])
|
|
];
|
|
}
|
|
break;
|
|
|
|
case 'revenue':
|
|
$revenue = $this->getRevenueStatistics($date_from, $date_to);
|
|
$headers = ['월', '주문수', '매출', '평균 주문금액'];
|
|
foreach ($revenue['monthly_revenue'] as $monthly) {
|
|
$rows[] = [
|
|
$monthly['month'],
|
|
$monthly['order_count'],
|
|
number_format($monthly['revenue']),
|
|
number_format($monthly['avg_order_value'])
|
|
];
|
|
}
|
|
break;
|
|
|
|
case 'expert_visits':
|
|
$visits = $this->getExpertVisitStatistics($date_from, $date_to);
|
|
$headers = ['전문가ID', '전문가명', '배정된 방문수', '완료된 방문수', '완료율(%)'];
|
|
foreach ($visits['expert_performance'] as $expert) {
|
|
$rows[] = [
|
|
$expert['expert_id'],
|
|
$expert['expert_name'],
|
|
$expert['assigned_visits'],
|
|
$expert['completed_visits'],
|
|
$expert['completion_rate']
|
|
];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Exception('유효하지 않은 리포트 타입입니다.');
|
|
}
|
|
|
|
// CSV 헤더 추가
|
|
$csv_data .= implode(',', $headers) . "\n";
|
|
|
|
// CSV 데이터 추가
|
|
foreach ($rows as $row) {
|
|
$csv_data .= implode(',', $row) . "\n";
|
|
}
|
|
|
|
return $csv_data;
|
|
|
|
} catch (Exception $e) {
|
|
error_log("EstimateManager::exportStatisticsCSV Error: " . $e->getMessage());
|
|
return '';
|
|
}
|
|
}
|
|
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}' 파일에 내용을 쓸 수 없습니다.");
|
|
}
|
|
}
|
|
}
|