first commit 2

This commit is contained in:
hmw1001
2026-06-11 18:47:38 +09:00
parent c768729ce6
commit 6f534e33a6
11095 changed files with 1595758 additions and 0 deletions
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());
}
}
}