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
+6
View File
@@ -0,0 +1,6 @@
<?php
define('G5_IS_ADMIN', true);
include_once ('../../common.php');
include_once(G5_ADMIN_PATH.'/admin.lib.php');
//add_stylesheet('<link rel="stylesheet" href="'.G5_SMS5_ADMIN_URL.'/css/sms5.css">', 0);
@@ -0,0 +1,144 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* 결제 솔루션의 핵심 비즈니스 로직을 담당하는 클래스
* 특정 서비스(예: 견적 시스템)에 종속되지 않는 범용적인 결제 기능을 제공합니다.
*/
class PaymentManager
{
private $payment_table = 'estimate_payment'; // 테이블 이름은 솔루션 접두사로 변경 가능
private $payment_log_table = 'estimate_payment_log';
private $payment_payload_table = 'estimate_payment_payload';
private $member; // 로그인한 사용자 정보
public function __construct()
{
global $member;
$this->member = $member;
}
// ==============================================================================
// 1. 결제 정보 생성 및 조회
// ==============================================================================
/**
* 새로운 결제 정보를 생성합니다.
* @param array $data 결제 데이터 (total_amount, payment_method 등)
* 'order_type', 'order_id' 등 연동 서비스의 정보를 추가할 수 있습니다.
* @return int|bool payment_id 또는 실패 시 false
*/
public function createPayment(array $data)
{
$data['payment_status'] = 'pending'; // 초기 상태는 'pending'
return $this->create($this->payment_table, $data);
}
/**
* payment_id로 결제 정보를 조회합니다.
* @param int $payment_id
* @return array|null
*/
public function getPaymentById(int $payment_id)
{
return sql_fetch("SELECT * FROM {$this->payment_table} WHERE id = '{$payment_id}' AND is_deleted = 0");
}
/**
* 특정 주문 ID (예: estimate_id)에 연결된 가장 최근의 pending 상태 결제 정보를 조회합니다.
* @param string $order_type 주문 타입 (예: 'estimate')
* @param int $order_id 주문 ID (예: estimate_id)
* @return array|null
*/
public function getLatestPendingPaymentByOrderId(string $order_type, int $order_id)
{
// 이 메서드는 payment 테이블에 order_type, order_id 컬럼이 추가되어야 동작합니다.
// 현재는 estimate_id 컬럼이 있으므로, estimate_id를 order_id로 간주합니다.
return sql_fetch("SELECT * FROM {$this->payment_table} WHERE estimate_id = '{$order_id}' AND payment_status = 'pending' AND is_deleted = 0 ORDER BY created_at DESC LIMIT 1");
}
// ==============================================================================
// 2. 결제 상태 업데이트 및 로그 기록
// ==============================================================================
/**
* 결제 상태를 업데이트하고 로그를 남깁니다.
* @param int $payment_id
* @param string $new_status
* @param string $memo
* @return bool
*/
public function updatePaymentStatus(int $payment_id, string $new_status, string $memo = '')
{
$old = sql_fetch("SELECT payment_status FROM {$this->payment_table} WHERE id = '{$payment_id}'");
if (!$old) return false;
$result = $this->update($this->payment_table, $payment_id, ["payment_status = '{$new_status}'"]);
if ($result) {
$this->logPaymentStatus($payment_id, $old['payment_status'], $new_status, $memo);
}
return $result;
}
/**
* PG사 전문(payload)을 기록합니다.
* @param int $payment_id
* @param string $type 'request', 'response', 'webhook' 등
* @param string $payload 원본 데이터
* @return int|bool payload_id 또는 실패 시 false
*/
public function logPaymentPayload(int $payment_id, string $type, string $payload)
{
$data = [
'payment_id' => $payment_id,
'payload_type' => $type,
'payload' => $payload
];
return $this->create($this->payment_payload_table, $data);
}
// ==============================================================================
// Private Helper Functions (공통 CRUD 및 로깅)
// ==============================================================================
private function create(string $table, array $data) : int|bool
{
$mb_id = $this->member['mb_id'] ?? 'system';
$data['created_by'] = $mb_id;
$data['created_at'] = G5_TIME_YMDHIS;
$data['is_used'] = $data['is_used'] ?? 1;
$data['is_deleted'] = $data['is_deleted'] ?? 0;
$fields = [];
$values = [];
foreach ($data as $key => $value) {
$fields[] = "`{$key}`";
$values[] = "'" . sql_real_escape_string($value) . "'";
}
$sql = "INSERT INTO {$table} (" . implode(', ', $fields) . ") VALUES (" . implode(', ', $values) . ")";
sql_query($sql);
return sql_insert_id();
}
private function update(string $table, int $id, array $set_clauses) : bool
{
$mb_id = $this->member['mb_id'] ?? 'system';
$set_clauses[] = "updated_at = '" . G5_TIME_YMDHIS . "'";
$set_clauses[] = "updated_by = '{$mb_id}'";
$sql = "UPDATE {$table} SET " . implode(', ', $set_clauses) . " WHERE id = '{$id}'";
return (bool)sql_query($sql);
}
private function logPaymentStatus(int $payment_id, string $old_status, string $new_status, string $memo) : int|bool
{
$data = [
'payment_id' => $payment_id,
'estimate_id' => sql_fetch("SELECT estimate_id FROM {$this->payment_table} WHERE id = '{$payment_id}'")['estimate_id'], // 연동을 위해 estimate_id도 저장
'previous_status' => $old_status,
'new_status' => $new_status,
'memo' => $memo,
];
return $this->create($this->payment_log_table, $data);
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
//$sub_menu = '800900'; // 메뉴 활성화를 위해
include_once('./_common.php');
include_once(__DIR__ . '/lib/SchemaManager.class.php');
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
function get_tables_from_sql_file($filepath) {
$tables = [];
if (!file_exists($filepath)) return $tables;
$lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (preg_match('/CREATE TABLE(?: IF NOT EXISTS)? `([^`]+)`/i', $line, $matches)) {
$tables[] = $matches[1];
}
}
return $tables;
}
$g5['title'] = '결제 관리 솔루션 설치';
include_once(G5_ADMIN_PATH . '/admin.head.php');
$install_result = null;
$delete_result = null;
$action = $_POST['action'] ?? '';
$tables_to_check = get_tables_from_sql_file(__DIR__ . '/install.sql');
if ($action === 'install') {
check_admin_token();
$sql_file = __DIR__ . '/install.sql';
$db_results = [];
try {
$schemaManager = new SchemaManager($sql_file);
$schemaManager->execute();
$db_results = $schemaManager->get_results();
} catch (Exception $e) { $db_results['errors'][] = $e->getMessage(); }
$install_result = ['db' => $db_results];
} else if ($action === 'delete') {
check_admin_token();
$delete_result = ['tables' => []];
$tables_to_delete = $tables_to_check;
foreach ($tables_to_delete as $table) {
sql_query("DROP TABLE IF EXISTS `{$table}`", false);
$delete_result['tables'][] = $table;
}
}
$existing_tables = [];
foreach ($tables_to_check as $table) {
if (sql_query("SHOW TABLES LIKE '$table'", false) && sql_num_rows(sql_query("SHOW TABLES LIKE '$table'", false)) > 0) {
$existing_tables[] = $table;
}
}
$is_installed = count($existing_tables) == count($tables_to_check);
?>
<style>
.install-container { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.install-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #AA20FF; }
.install-header h1 { color: #AA20FF; margin-bottom: 10px; }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0; }
.feature-card { padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center; }
.feature-card i { font-size: 2em; color: #AA20FF; margin-bottom: 10px; }
.status-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.status-table th, .status-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
.status-table th { background-color: #fff; font-weight: bold; }
.status-ok { color: #28a745; font-weight: bold; }
.status-missing { color: #dc3545; font-weight: bold; }
.install-btn { display: block; width: 200px; margin: 30px auto; padding: 15px 30px; background: #AA20FF; color: white; text-align: center; text-decoration: none; border-radius: 5px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; transition: background-color 0.3s; }
.install-btn:hover { background: #8A1ACC; color: white; }
.install-btn:disabled { background: #ccc; cursor: not-allowed; }
.alert { padding: 15px; margin: 20px 0; border-radius: 5px; }
.alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.alert-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
.alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
.btn-secondary { background: #6c757d; color: white; border-color: #6c757d; padding: 5px 10px; border-radius: 4px; text-decoration: none; }
.btn-secondary:hover { background: #5a6268; }
.btn-danger { background: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
.btn-danger:hover { background: #c82333; }
.button-group { display: flex; justify-content: center; align-items: center; gap: 10px; }
</style>
<div class="install-container">
<div class="install-header">
<h1><i class="fa fa-credit-card"></i> 결제 관리 솔루션</h1>
<p>다양한 결제 모듈 연동 및 로그 관리 시스템</p>
</div>
<?php if ($install_result): ?>
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 작업 완료</h4><p>데이터베이스 설치 작업이 완료되었습니다.</p><p><a href="<?php echo G5_ADMIN_URL; ?>" class="btn btn-primary">관리자 메인으로 이동</a></p></div>
<?php elseif ($delete_result): ?>
<div class="alert alert-danger"><h4><i class="fa fa-trash"></i> 삭제 작업 완료</h4><p>솔루션 관련 데이터가 삭제되었습니다.</p><ul><?php foreach($delete_result['tables'] as $tbl) echo "<li>{$tbl} 테이블 삭제됨</li>"; ?></ul></div>
<?php elseif ($is_installed): ?>
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 완료</h4><p>결제 관리 솔루션이 이미 설치되어 있습니다.</p><p><a href="<?php echo G5_ADMIN_URL; ?>" class="btn btn-primary">관리자 메인으로 이동</a></p></div>
<?php else: ?>
<div class="alert alert-info"><h4><i class="fa fa-info-circle"></i> 설치 필요</h4><p>결제 관리 솔루션을 사용하기 위해 설치가 필요합니다.</p></div>
<?php endif; ?>
<h3><i class="fa fa-database"></i> 설치 상태</h3>
<table class="status-table">
<thead><tr><th>테이블명</th><th>설명</th><th>상태</th></tr></thead>
<tbody>
<?php foreach ($tables_to_check as $table): ?>
<tr>
<td><code><?php echo $table; ?></code></td>
<td><?php echo array('estimate_payment' => '결제 정보', 'estimate_payment_log' => '결제 상태 로그', 'estimate_payment_payload' => 'PG사 연동 전문')[$table] ?? '데이터 테이블'; ?></td>
<td>
<?php if (in_array($table, $existing_tables)): ?>
<span class="status-ok"><i class="fa fa-check"></i> 설치됨</span>
<?php else: ?>
<span class="status-missing"><i class="fa fa-times"></i> 미설치</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$is_installed): ?>
<form method="post" onsubmit="return confirm('솔루션을 설치하시겠습니까?');">
<input type="hidden" name="action" value="install">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="install-btn"><i class="fa fa-download"></i> 솔루션 설치하기</button>
</form>
<?php endif; ?>
<?php if ($is_installed && !$install_result && !$delete_result): ?>
<div class="button-group" style="text-align: center; margin-top: 20px;">
<form method="post" onsubmit="return confirm('기존 데이터는 유지되며, 변경된 DB 구조만 업데이트 됩니다. 진행하시겠습니까?');">
<input type="hidden" name="action" value="install">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="btn btn-secondary"><i class="fa fa-sync"></i> 재설치 (업데이트)</button>
</form>
<form method="post" onsubmit="return confirm('정말로 솔루션을 삭제하시겠습니까? 모든 관련 데이터가 영구적으로 삭제됩니다.');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="btn-danger"><i class="fa fa-trash"></i> 솔루션 삭제하기</button>
</form>
</div>
<?php endif; ?>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+87
View File
@@ -0,0 +1,87 @@
-- 3. 견적서 결제 정보
CREATE TABLE IF NOT EXISTS `estimate_payment`
(
`id` int NOT NULL AUTO_INCREMENT,
`estimate_id` int NOT NULL COMMENT '견적서 ID',
`payment_method` varchar(50) COMMENT '결제 수단 (pg, bank, cash)',
`total_amount` int NOT NULL COMMENT '최종 결제 요청 금액',
`payment_status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '결제 상태 (pending:요청, completed:완료, failed:실패, cancelled:취소)',
`pg_tid` varchar(100) COMMENT 'PG사 거래 ID',
`pg_receipt_url` varchar(255) COMMENT 'PG사 영수증 URL',
`bank_account` varchar(100) COMMENT '무통장 입금 계좌',
`depositor_name` varchar(50) COMMENT '입금자명',
`temp_1` varchar(255) DEFAULT NULL,
`temp_2` varchar(255) DEFAULT NULL,
`temp_3` varchar(255) DEFAULT NULL,
`temp_4` varchar(255) DEFAULT NULL,
`temp_5` varchar(255) DEFAULT NULL,
`extra_1` varchar(255) DEFAULT NULL,
`extra_2` varchar(255) DEFAULT NULL,
`extra_3` varchar(255) DEFAULT NULL,
`extra_4` varchar(255) DEFAULT NULL,
`extra_5` varchar(255) DEFAULT NULL,
`is_used` tinyint(1) NOT NULL DEFAULT '1',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` varchar(20) DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`updated_by` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `estimate_id` (`estimate_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE=utf8mb4_general_ci COMMENT ='견적서 결제 정보';
-- 4. 결제 상태 변경 이력 (로그 테이블)
CREATE TABLE IF NOT EXISTS `estimate_payment_log`
(
`id` int NOT NULL AUTO_INCREMENT,
`payment_id` int NOT NULL COMMENT '결제 정보 ID',
`estimate_id` int NOT NULL COMMENT '견적서 ID',
`previous_status` varchar(50) DEFAULT NULL COMMENT '이전 상태',
`new_status` varchar(50) NOT NULL COMMENT '새 상태',
`memo` varchar(255) DEFAULT NULL COMMENT '변경 사유 또는 메모',
`temp_1` varchar(255) DEFAULT NULL,
`temp_2` varchar(255) DEFAULT NULL,
`temp_3` varchar(255) DEFAULT NULL,
`temp_4` varchar(255) DEFAULT NULL,
`temp_5` varchar(255) DEFAULT NULL,
`extra_1` varchar(255) DEFAULT NULL,
`extra_2` varchar(255) DEFAULT NULL,
`extra_3` varchar(255) DEFAULT NULL,
`extra_4` varchar(255) DEFAULT NULL,
`extra_5` varchar(255) DEFAULT NULL,
`is_used` tinyint(1) NOT NULL DEFAULT '1',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` varchar(20) NOT NULL COMMENT '변경자 ID',
`updated_at` datetime DEFAULT NULL,
`updated_by` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `payment_id` (`payment_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE=utf8mb4_general_ci COMMENT ='결제 상태 변경 이력';
-- 5. PG사 연동 전문 (로그) 테이블
CREATE TABLE IF NOT EXISTS `estimate_payment_payload`
(
`id` int NOT NULL AUTO_INCREMENT,
`payment_id` int NOT NULL COMMENT '결제 정보 ID',
`payload_type` VARCHAR(20) NOT NULL COMMENT '전문 종류 (request, response, webhook 등)',
`payload` LONGTEXT COMMENT 'PG사에서 받은 원본 전문 (JSON, XML 등)',
`temp_1` varchar(255) DEFAULT NULL,
`temp_2` varchar(255) DEFAULT NULL,
`temp_3` varchar(255) DEFAULT NULL,
`temp_4` varchar(255) DEFAULT NULL,
`temp_5` varchar(255) DEFAULT NULL,
`extra_1` varchar(255) DEFAULT NULL,
`extra_2` varchar(255) DEFAULT NULL,
`extra_3` varchar(255) DEFAULT NULL,
`extra_4` varchar(255) DEFAULT NULL,
`extra_5` varchar(255) DEFAULT NULL,
`is_used` tinyint(1) NOT NULL DEFAULT '1',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` varchar(20) DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`updated_by` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `payment_id` (`payment_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE=utf8mb4_general_ci COMMENT ='PG사 연동 전문';
@@ -0,0 +1,200 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* SQL 파일을 기반으로 데이터베이스 스키마를 관리(생성/업데이트)하는 범용 클래스
*/
class SchemaManager
{
private $sql_file_path;
private $results;
private $conn; // 추가: DB 연결 객체를 저장할 변수
/**
* 생성자
* @param string $sql_file_path install.sql 파일의 절대 경로
*/
public function __construct($sql_file_path)
{
global $g5; // 그누보드 DB 연결 객체에 접근하기 위해 global 선언
$this->conn = $g5['connect_db']; // DB 연결 객체를 저장
if (!file_exists($sql_file_path)) {
throw new Exception($sql_file_path . ' 파일을 찾을 수 없습니다.');
}
$this->sql_file_path = $sql_file_path;
$this->results = [
'created' => [],
'existing' => [],
'updated' => [],
'failed' => [],
'errors' => [],
];
}
/**
* 스키마 설치/업데이트를 실행합니다.
*/
public function execute()
{
$sql_statements = $this->parse_sql_file();
foreach ($sql_statements as $stmt) {
// CREATE TABLE 문인지 확인
if (preg_match('/^CREATE\s+TABLE/i', $stmt)) {
$schema = $this->parse_create_table_sql($stmt);
if ($schema && !empty($schema['name'])) {
$this->process_table_schema($stmt, $schema);
}
} else {
// CREATE TABLE 문이 아닌 다른 SQL 문 (e.g. INSERT, UPDATE)
// 이 부분도 mysqli_query를 사용하도록 변경
mysqli_query($this->conn, $stmt);
}
}
}
/**
* 처리 결과를 반환합니다.
* @return array
*/
public function get_results()
{
return $this->results;
}
/**
* 테이블 스키마를 처리합니다. (생성 또는 업데이트)
* @param string $create_sql 전체 CREATE TABLE 구문
* @param array $schema 파싱된 스키마 정보
*/
private function process_table_schema($create_sql, $schema)
{
$table_name = $schema['name'];
if ($this->table_exists($table_name)) {
// 테이블이 존재하면, 컬럼 비교 및 추가
$this->results['existing'][] = $table_name;
$this->update_table_columns($table_name, $schema['columns']);
} else {
// 테이블이 존재하지 않으면, 새로 생성
if (mysqli_query($this->conn, $create_sql)) { // sql_query 대신 mysqli_query 사용
$this->results['created'][] = $table_name;
} else {
$this->results['failed'][] = $table_name;
$this->results['errors'][] = "<strong>{$table_name} 테이블 생성 실패</strong>: " . mysqli_error($this->conn); // sql_error 대신 mysqli_error 사용
}
}
}
/**
* 테이블의 컬럼 구조를 업데이트합니다.
* @param string $table_name
* @param array $target_columns .sql 파일에 정의된 컬럼 목록
*/
private function update_table_columns($table_name, $target_columns)
{
$current_columns = $this->get_current_columns($table_name);
$added_columns_in_table = [];
foreach ($target_columns as $col_name => $col_definition) {
// 현재 테이블에 해당 컬럼이 없으면 추가
if (!isset($current_columns[$col_name])) {
$alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$col_name}` {$col_definition}";
if (mysqli_query($this->conn, $alter_sql)) {
$added_columns_in_table[] = $col_name;
} else {
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
$this->results['errors'][] = "<strong>{$table_name} 테이블에 '{$col_name}' 컬럼 추가 실패</strong>: " . mysqli_error($this->conn);
}
}
// 컬럼이 이미 존재하면 아무것도 하지 않습니다.
}
if (!empty($added_columns_in_table)) {
$this->results['updated'][$table_name] = $added_columns_in_table;
}
}
/**
* SQL 파일을 읽고 각 구문으로 분리합니다.
* @return array
*/
private function parse_sql_file()
{
$sql = file_get_contents($this->sql_file_path);
// 주석 제거 (SQL 주석 '--' 와 C-style '/* ... */' 주석)
$sql = preg_replace('/--.*/', '', $sql);
$sql = preg_replace('!/\*.*?\*/!s', '', $sql);
$sql = trim($sql);
// 세미콜론(;)을 기준으로 쿼리 분리
return array_filter(array_map('trim', explode(';', $sql)));
}
/**
* 테이블 존재 여부를 확인합니다.
* @param string $table_name
* @return bool
*/
private function table_exists($table_name)
{
$res = mysqli_query($this->conn, "SHOW TABLES LIKE '{$table_name}'");
return mysqli_num_rows($res) > 0;
}
/**
* 현재 DB에 있는 테이블의 컬럼 목록을 가져옵니다.
* @param string $table_name
* @return array
*/
private function get_current_columns($table_name)
{
$res = mysqli_query($this->conn, "SHOW COLUMNS FROM `{$table_name}`");
$columns = [];
while ($row = mysqli_fetch_array($res)) {
$columns[$row['Field']] = true;
}
return $columns;
}
/**
* CREATE TABLE SQL 구문에서 테이블명과 컬럼 정의를 파싱합니다.
* @param string $query CREATE TABLE 구문
* @return array|null
*/
private function parse_create_table_sql($query)
{
$table_name = '';
if (preg_match('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?(\w+)`?/i', $query, $matches)) {
$table_name = $matches[1];
} else {
return null;
}
// 괄호 안의 내용만 추출
$start = strpos($query, '(');
$end = strrpos($query, ')');
if ($start === false || $end === false) {
return ['name' => $table_name, 'columns' => []];
}
$content = substr($query, $start + 1, $end - $start - 1);
// 줄 단위로 분리
$lines = explode("\n", $content);
$columns = [];
foreach ($lines as $line) {
$line = trim($line, " \t\n\r\0\x0B,");
// 컬럼 정의 라인인지 확인 (첫 단어가 `column_name` 형태)
if (preg_match('/^`(\w+)`\s+(.*)/i', $line, $match)) {
$col_name = $match[1];
$col_definition = $match[2];
$columns[$col_name] = $col_definition;
}
}
return ['name' => $table_name, 'columns' => $columns];
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
$sub_menu = '400900';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$od_id = $_GET['od_id'] ?? '';
$qstr = 'sfl='.$_GET['sfl'].'&stx='.$_GET['stx'].'&sod='.$_GET['sod'].'&spt='.$_GET['spt'].'&page='.$_GET['page'];
$sql = " SELECT * FROM {$g5['g5_shop_order_table']} WHERE od_id = '{$od_id}' ";
$order = sql_fetch($sql);
if (!$order['od_id']) {
alert('존재하지 않는 주문입니다.');
}
$g5['title'] = '결제 상세 정보';
include_once(G5_ADMIN_PATH.'/admin.head.php');
?>
<div class="tbl_frm01 tbl_wrap">
<table>
<caption>결제 상세 정보</caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row">주문번호</th>
<td><?php echo $order['od_id']; ?></td>
</tr>
<tr>
<th scope="row">주문자</th>
<td><?php echo $order['od_name']; ?> (<?php echo $order['mb_id'] ? $order['mb_id'] : '비회원'; ?>)</td>
</tr>
<tr>
<th scope="row">결제금액</th>
<td><?php echo number_format($order['od_receipt_price']); ?>원</td>
</tr>
<tr>
<th scope="row">결제수단</th>
<td><?php echo $order['od_settle_case']; ?></td>
</tr>
<tr>
<th scope="row">결제상태</th>
<td><?php echo $order['od_status']; ?></td>
</tr>
<tr>
<th scope="row">주문일시</th>
<td><?php echo $order['od_time']; ?></td>
</tr>
<?php if ($order['od_pg'] && $order['od_tno']): ?>
<tr>
<th scope="row">PG사</th>
<td><?php echo $order['od_pg']; ?></td>
</tr>
<tr>
<th scope="row">거래번호 (TID)</th>
<td><?php echo $order['od_tno']; ?></td>
</tr>
<tr>
<th scope="row">결제 취소</th>
<td>
<form name="fcancel" action="./pg_cancel.php" method="post" onsubmit="return fcancel_check(this);">
<input type="hidden" name="od_id" value="<?php echo $order['od_id']; ?>">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<label for="cancel_memo">취소 사유:</label>
<input type="text" name="cancel_memo" id="cancel_memo" required class="frm_input" size="30" value="관리자 취소">
<label for="cancel_amount" class="ml-3">취소 금액:</label>
<input type="number" name="cancel_amount" id="cancel_amount" class="frm_input" size="10" placeholder="전체취소는 0 또는 미입력">
<input type="submit" value="결제 취소 실행" class="btn_submit btn">
</form>
<div class="local_desc02 local_desc">
<p>
PG사를 통해 결제된 건에 한해 취소가 가능합니다.<br>
<strong>취소 금액</strong>을 입력하지 않거나 0으로 입력하면 <strong>전체 취소</strong>됩니다.<br>
실행 즉시 PG사에 취소 요청이 전송되므로 신중하게操作하십시오.
</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="btn_fixed_top">
<a href="./payment_list.php?<?php echo $qstr; ?>" class="btn btn_02">목록</a>
</div>
<script>
function fcancel_check(f) {
var amount = parseInt(f.cancel_amount.value.replace(/[^0-9]/g, ""));
var max_amount = parseInt("<?php echo $order['od_receipt_price']; ?>");
if (amount > 0 && amount > max_amount) {
alert("취소 금액은 결제 금액보다 클 수 없습니다.");
return false;
}
var msg = "이 결제를 정말로 취소하시겠습니까?";
if (amount > 0) {
msg = "이 결제 건에 대해 " + amount + "원을 부분 취소하시겠습니까?";
}
return confirm(msg);
}
</script>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+118
View File
@@ -0,0 +1,118 @@
<?php
$sub_menu = '400900'; // 임시 메뉴 코드 (적절히 변경 필요)
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$g5['title'] = '결제 내역 관리';
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 검색 조건
$sfl = $_GET['sfl'] ?? '';
$stx = $_GET['stx'] ?? '';
$sod = $_GET['sod'] ?? '';
$spt = $_GET['spt'] ?? '';
$page = $_GET['page'] ?? 1;
$where = " WHERE 1 ";
if ($stx) {
if ($sfl === 'od_id') {
$where .= " AND od_id LIKE '%{$stx}%' ";
} else if ($sfl === 'mb_id') {
$where .= " AND mb_id LIKE '%{$stx}%' ";
} else if ($sfl === 'od_name') {
$where .= " AND od_name LIKE '%{$stx}%' ";
}
}
$sql_common = " FROM {$g5['g5_shop_order_table']} {$where} ";
$sql_order = " ORDER BY od_id DESC ";
$row_count = sql_fetch(" SELECT COUNT(*) as cnt {$sql_common} ");
$total_count = $row_count['cnt'];
$rows = $config['cf_page_rows'];
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
$from_record = ($page - 1) * $rows; // 시작 레코드 구함
$sql = " SELECT * {$sql_common} {$sql_order} LIMIT {$from_record}, {$rows} ";
$result = sql_query($sql);
$qstr = 'sfl='.$sfl.'&stx='.$stx.'&sod='.$sod.'&spt='.$spt;
?>
<div class="local_sch01 local_sch">
<form name="fsearch" id="fsearch" class="local_sch_search" method="get">
<label for="sfl" class="sound_only">검색대상</label>
<select name="sfl" id="sfl">
<option value="od_id" <?php echo get_selected($sfl, 'od_id'); ?>>주문번호</option>
<option value="mb_id" <?php echo get_selected($sfl, 'mb_id'); ?>>회원ID</option>
<option value="od_name" <?php echo get_selected($sfl, 'od_name'); ?>>주문자명</option>
</select>
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
<input type="text" name="stx" value="<?php echo $stx; ?>" id="stx" required class="frm_input">
<input type="submit" value="검색" class="btn_submit">
</form>
</div>
<div class="local_ov01 local_ov">
전체 <?php echo number_format($total_count); ?> 건
</div>
<div class="tbl_head01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?> 목록</caption>
<thead>
<tr>
<th scope="col">주문번호</th>
<th scope="col">주문자</th>
<th scope="col">결제금액</th>
<th scope="col">결제수단</th>
<th scope="col">결제상태</th>
<th scope="col">주문일시</th>
<th scope="col">관리</th>
</tr>
</thead>
<tbody>
<?php
for ($i=0; $row=sql_fetch_array($result); $i++) {
$bg = 'bg'.($i%2);
?>
<tr class="<?php echo $bg; ?>">
<td class="td_num">
<a href="./payment_form.php?od_id=<?php echo $row['od_id']; ?>&amp;<?php echo $qstr; ?>&amp;page=<?php echo $page; ?>"><?php echo $row['od_id']; ?></a>
</td>
<td class="td_mb_id">
<?php echo $row['od_name']; ?> (<?php echo $row['mb_id'] ? $row['mb_id'] : '비회원'; ?>)
</td>
<td class="td_numbig">
<?php echo number_format($row['od_receipt_price']); ?>원
</td>
<td class="td_center">
<?php echo $row['od_settle_case']; ?>
</td>
<td class="td_center">
<?php echo $row['od_status']; ?>
</td>
<td class="td_datetime">
<?php echo $row['od_time']; ?>
</td>
<td class="td_mng">
<a href="./payment_form.php?od_id=<?php echo $row['od_id']; ?>&amp;<?php echo $qstr; ?>&amp;page=<?php echo $page; ?>" class="btn btn_03">상세</a>
</td>
</tr>
<?php
}
if ($total_count == 0) {
echo '<tr><td colspan="7" class="empty_table">데이터가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<?php echo get_paging_admin(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_page_rows'], $page, $total_page, $_SERVER['SCRIPT_NAME'].'?'.$qstr.'&amp;page='); ?>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+96
View File
@@ -0,0 +1,96 @@
<?php
include_once('./_common.php');
if (!$is_admin) {
alert('관리자만 접근 가능합니다.');
}
check_admin_token();
$od_id = $_POST['od_id'] ?? '';
$cancel_memo = $_POST['cancel_memo'] ?? '관리자 취소';
$cancel_amount = isset($_POST['cancel_amount']) ? (int)$_POST['cancel_amount'] : 0;
if (!$od_id) {
alert('주문번호가 없습니다.');
}
$sql = " SELECT * FROM {$g5['g5_shop_order_table']} WHERE od_id = '{$od_id}' ";
$order = sql_fetch($sql);
if (!$order['od_id']) {
alert('존재하지 않는 주문입니다.');
}
if (!$order['od_pg'] || !$order['od_tno']) {
alert('PG 결제 정보가 없어 취소할 수 없습니다.');
}
// PG사에 따라 필요한 변수 설정
$tno = $order['od_tno'];
$od_pg = $order['od_pg'];
// 부분취소 여부 및 금액 설정
$is_partial = false;
if ($cancel_amount > 0) {
if ($cancel_amount > $order['od_receipt_price']) {
alert('취소 금액은 결제 금액보다 클 수 없습니다.');
}
$is_partial = true;
$amount = $cancel_amount;
} else {
$amount = $order['od_receipt_price'];
}
// PG사별 취소 로직 분기
switch ($od_pg) {
case 'kcp':
$_POST['req_tx'] = 'mod';
$_POST['mod_type'] = $is_partial ? 'STPC' : 'STSC'; // 부분취소: STPC, 전체취소: STSC
$_POST['mod_id'] = $od_id;
$_POST['mod_mny'] = $amount;
$_POST['rem_mny'] = $is_partial ? ($order['od_receipt_price'] - $amount) : 0;
include_once(G5_SHOP_PATH.'/kcp/pp_ax_hub_cancel.php');
break;
case 'lg':
$_POST['LGD_TID'] = $tno;
if ($is_partial) {
$_POST['LGD_TXNAME'] = 'PartialCancel';
$_POST['LGD_CANCELAMOUNT'] = $amount;
} else {
$_POST['LGD_TXNAME'] = 'Cancel';
}
include_once(G5_SHOP_PATH.'/lg/xpay_cancel.php');
break;
case 'inicis':
$_POST['type'] = 'cancel';
$_POST['mid'] = $default['de_inicis_mid'];
$_POST['tid'] = $tno;
$_POST['msg'] = $cancel_memo;
if ($is_partial) {
$_POST['price'] = $amount; // 부분취소 금액
$_POST['confirm_price'] = $order['od_receipt_price'] - $amount; // 남은 금액
$_POST['partial_cancel'] = '1';
}
include_once(G5_SHOP_PATH.'/inicis/inipay_cancel.php');
break;
case 'nicepay':
$cancelAmt = $amount;
$partialCancelCode = $is_partial ? '1' : '0'; // 부분취소: 1, 전체취소: 0
include_once(G5_SHOP_PATH.'/nicepay/cancel_process.php');
break;
default:
alert('지원하지 않는 PG사입니다.');
break;
}
alert('PG사에 취소 요청을 전송했습니다. 최종 결과는 PG사 관리자 페이지에서 확인해주세요.', './payment_list.php');
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
include_once('./_common.php');
include_once(__DIR__ . '/classes/PaymentManager.class.php');
// PG사 연동 라이브러리 (예시: 실제 PG사에 맞게 구현 필요)
// include_once(G5_LIB_PATH . '/pg_lib.php');
$payment_id = isset($_POST['payment_id']) ? (int)$_POST['payment_id'] : 0;
$estimate_id = isset($_POST['estimate_id']) ? (int)$_POST['estimate_id'] : 0;
$total_amount = isset($_POST['total_amount']) ? (int)$_POST['total_amount'] : 0;
$payment_method = isset($_POST['payment_method']) ? trim($_POST['payment_method']) : '';
$depositor_name = isset($_POST['depositor_name']) ? trim($_POST['depositor_name']) : ''; // 무통장 입금자명
if (!$payment_id || !$estimate_id || !$total_amount || !$payment_method) {
alert('필수 정보가 누락되었습니다.');
}
$paymentManager = new PaymentManager();
$payment_info = $paymentManager->getPaymentById($payment_id);
if (!$payment_info || $payment_info['estimate_id'] != $estimate_id || $payment_info['total_amount'] != $total_amount) {
alert('유효하지 않은 결제 정보입니다.');
}
try {
// 결제 수단에 따른 처리
if ($payment_method === 'pg') {
// PG사 결제 요청 로직
// 실제 PG사 연동 코드가 들어갈 부분입니다.
// 예: 이니시스, KG이니시스, 나이스페이 등 PG사 SDK 호출
// 가상의 PG 요청 데이터
$pg_request_data = [
'order_id' => $payment_id,
'amount' => $total_amount,
'item_name' => '견적서 결제',
'buyer_name' => $member['mb_name'] ?? '비회원',
'buyer_email' => $member['mb_email'] ?? '',
// ... 기타 PG사 요구 파라미터 ...
];
// PG사 요청 전문 저장
$paymentManager->logPaymentPayload($payment_id, 'request', json_encode($pg_request_data, JSON_UNESCAPED_UNICODE));
// [중요] 실제 PG사 결제창 호출 또는 리다이렉션
// 여기서는 예시로 성공 페이지로 바로 리다이렉트합니다.
// 실제 구현에서는 PG사 결제창을 띄우거나, PG사 API를 호출하여 결제 프로세스를 시작합니다.
// 결제 완료 후 pg_return.php로 결과가 돌아오도록 설정해야 합니다.
goto_url(G5_ADMIN_URL . '/payment_manage/pg_return.php?payment_id=' . $payment_id . '&status=success&tid=TEST_TID_'.time());
} else if ($payment_method === 'bank') {
// 무통장 입금 처리
$set_clauses = [
"payment_method = 'bank'",
"depositor_name = '" . sql_real_escape_string($depositor_name) . "'",
// 계좌 정보는 고정된 값으로 가정하거나, 관리자 설정에서 가져올 수 있습니다.
"bank_account = '우리은행 1002-XXX-XXXXXX'"
];
$paymentManager->update($paymentManager->payment_table, $payment_id, $set_clauses);
$paymentManager->updatePaymentStatus($payment_id, 'pending', '무통장 입금 신청');
alert('무통장 입금 신청이 완료되었습니다. 입금 확인 후 처리됩니다.', G5_URL);
} else if ($payment_method === 'cash') {
// 현장 결제 처리 (관리자가 수동으로 확인해야 함)
$set_clauses = [
"payment_method = 'cash'"
];
$paymentManager->update($paymentManager->payment_table, $payment_id, $set_clauses);
$paymentManager->updatePaymentStatus($payment_id, 'pending', '현장 결제 신청');
alert('현장 결제 신청이 완료되었습니다. 관리자 확인 후 처리됩니다.', G5_URL);
}
} catch (Exception $e) {
error_log("PG 요청 중 오류 발생: " . $e->getMessage());
alert('결제 처리 중 오류가 발생했습니다: ' . $e->getMessage());
}
+69
View File
@@ -0,0 +1,69 @@
<?php
include_once('./_common.php');
include_once(__DIR__ . '/classes/PaymentManager.class.php');
// PG사 연동 라이브러리 (예시: 실제 PG사에 맞게 구현 필요)
// include_once(G5_LIB_PATH . '/pg_lib.php');
$payment_id = isset($_REQUEST['payment_id']) ? (int)$_REQUEST['payment_id'] : 0;
$pg_status = isset($_REQUEST['status']) ? trim($_REQUEST['status']) : ''; // PG사에서 넘겨주는 결제 상태 (success, fail 등)
$pg_tid = isset($_REQUEST['tid']) ? trim($_REQUEST['tid']) : ''; // PG사 거래 고유 ID
$pg_response_data = json_encode($_REQUEST, JSON_UNESCAPED_UNICODE); // PG사에서 받은 모든 응답 데이터
if (!$payment_id || !$pg_status) {
alert('잘못된 접근입니다. 필수 정보가 누락되었습니다.');
}
$paymentManager = new PaymentManager();
$payment_info = $paymentManager->getPaymentById($payment_id);
if (!$payment_info) {
alert('유효하지 않은 결제 정보입니다.');
}
try {
// PG사 응답 전문 저장
$paymentManager->logPaymentPayload($payment_id, 'response', $pg_response_data);
$new_payment_status = 'failed'; // 기본은 실패
$memo = 'PG사 결제 실패';
if ($pg_status === 'success') {
// [중요] PG사로부터 받은 실제 결제 결과 검증 로직
// 이 부분은 PG사 연동 가이드에 따라 서버에서 다시 한번 결제 위변조 여부를 검증해야 합니다.
// 예: PG사 API를 호출하여 최종 결제 상태 확인
// 검증 성공 가정
$new_payment_status = 'completed';
$memo = 'PG사 결제 성공';
// payment 테이블에 PG사 거래 ID 업데이트
$paymentManager->update($paymentManager->payment_table, $payment_id, ["pg_tid = '" . sql_real_escape_string($pg_tid) . "'"]);
} else {
// PG사 결제 실패 처리
$memo = 'PG사 결제 실패: ' . $pg_status;
}
// 결제 상태 업데이트
$result = $paymentManager->updatePaymentStatus($payment_id, $new_payment_status, $memo);
if ($result) {
if ($new_payment_status === 'completed') {
// [알림] 결제 완료 알림 (고객, 관리자, 대리점 등)
if (false) { // $paymentManager->sendPaymentCompletionNotification($payment_info['estimate_id']);
}
alert('결제가 성공적으로 완료되었습니다.', G5_URL);
} else {
alert('결제에 실패했습니다. 다시 시도해주세요.', G5_URL);
}
} else {
alert('결제 상태 업데이트에 실패했습니다. 관리자에게 문의하세요.', G5_URL);
}
} catch (Exception $e) {
error_log("PG 결과 처리 중 오류 발생: " . $e->getMessage());
alert('결제 결과 처리 중 오류가 발생했습니다: ' . $e->getMessage());
}
exit;
+134
View File
@@ -0,0 +1,134 @@
<?php
$sub_menu = '400910'; // 임시 메뉴 코드
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$g5['title'] = '정산/통계 관리';
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 검색 기간 설정 (기본값: 이번 달)
$fr_date = $_GET['fr_date'] ?? date('Y-m-01');
$to_date = $_GET['to_date'] ?? date('Y-m-d');
// 1. 일별 매출 통계
$sql = " SELECT
SUBSTRING(od_time, 1, 10) as od_date,
COUNT(*) as cnt,
SUM(od_receipt_price) as total_price,
SUM(od_cancel_price) as cancel_price
FROM {$g5['g5_shop_order_table']}
WHERE od_time BETWEEN '{$fr_date} 00:00:00' AND '{$to_date} 23:59:59'
AND od_status IN ('입금', '준비', '배송', '완료')
GROUP BY od_date
ORDER BY od_date DESC ";
$result = sql_query($sql);
$daily_data = [];
$total_sum = 0;
$total_cnt = 0;
while ($row = sql_fetch_array($result)) {
$daily_data[] = $row;
$total_sum += $row['total_price'];
$total_cnt += $row['cnt'];
}
// 2. 결제 수단별 통계
$sql_settle = " SELECT
od_settle_case,
COUNT(*) as cnt,
SUM(od_receipt_price) as total_price
FROM {$g5['g5_shop_order_table']}
WHERE od_time BETWEEN '{$fr_date} 00:00:00' AND '{$to_date} 23:59:59'
AND od_status IN ('입금', '준비', '배송', '완료')
GROUP BY od_settle_case ";
$result_settle = sql_query($sql_settle);
?>
<div class="local_sch01 local_sch">
<form name="fsearch" id="fsearch" class="local_sch_search" method="get">
<span class="sch_tit">기간</span>
<input type="text" name="fr_date" value="<?php echo $fr_date; ?>" id="fr_date" class="frm_input" size="11" maxlength="10">
~
<input type="text" name="to_date" value="<?php echo $to_date; ?>" id="to_date" class="frm_input" size="11" maxlength="10">
<input type="submit" value="검색" class="btn_submit">
</form>
</div>
<script>
$(function(){
$("#fr_date, #to_date").datepicker({ changeMonth: true, changeYear: true, dateFormat: "yy-mm-dd", showButtonPanel: true, yearRange: "c-99:c+99", maxDate: "+0d" });
});
</script>
<div class="tbl_head01 tbl_wrap">
<table>
<caption>일별 매출 현황</caption>
<thead>
<tr>
<th scope="col">날짜</th>
<th scope="col">주문건수</th>
<th scope="col">결제금액</th>
<th scope="col">취소금액</th>
<th scope="col">순매출</th>
</tr>
</thead>
<tbody>
<?php foreach ($daily_data as $row):
$net_sales = $row['total_price'] - $row['cancel_price'];
?>
<tr>
<td class="td_date"><?php echo $row['od_date']; ?></td>
<td class="td_num"><?php echo number_format($row['cnt']); ?>건</td>
<td class="td_numbig"><?php echo number_format($row['total_price']); ?>원</td>
<td class="td_numbig"><?php echo number_format($row['cancel_price']); ?>원</td>
<td class="td_numbig" style="font-weight:bold; color:#0056b3;"><?php echo number_format($net_sales); ?>원</td>
</tr>
<?php endforeach; ?>
<?php if (empty($daily_data)): ?>
<tr><td colspan="5" class="empty_table">데이터가 없습니다.</td></tr>
<?php else: ?>
<tr class="bg1">
<td class="td_date"><strong>합계</strong></td>
<td class="td_num"><strong><?php echo number_format($total_cnt); ?>건</strong></td>
<td class="td_numbig"><strong><?php echo number_format($total_sum); ?>원</strong></td>
<td class="td_numbig">-</td>
<td class="td_numbig"><strong><?php echo number_format($total_sum); ?>원</strong></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="tbl_head01 tbl_wrap" style="margin-top:30px;">
<table>
<caption>결제 수단별 통계</caption>
<thead>
<tr>
<th scope="col">결제수단</th>
<th scope="col">건수</th>
<th scope="col">금액</th>
<th scope="col">비율</th>
</tr>
</thead>
<tbody>
<?php
while ($row = sql_fetch_array($result_settle)) {
$ratio = $total_sum > 0 ? round(($row['total_price'] / $total_sum) * 100, 1) : 0;
?>
<tr>
<td class="td_center"><?php echo $row['od_settle_case']; ?></td>
<td class="td_num"><?php echo number_format($row['cnt']); ?>건</td>
<td class="td_numbig"><?php echo number_format($row['total_price']); ?>원</td>
<td class="td_num"><?php echo $ratio; ?>%</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>