From 5bd9cd2d17a8aee8121e5d9d8db8784bd94b0a37 Mon Sep 17 00:00:00 2001 From: whitedot Date: Wed, 9 Sep 2026 07:37:46 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=83=81=ED=92=88=EB=B3=84=20=EB=B0=B0?= =?UTF-8?q?=EC=86=A1=EC=99=84=EB=A3=8C=20=EC=8B=9C=EA=B0=81=EC=9D=84=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A7=80=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개별·일괄 완료 처리에서 완료 시각을 기록하고 해당 시각부터 지급 대기 기간을 계산한다. 기존 완료 상품은 종전 기준을 유지하며 0일 설정은 완료 처리 직후 지급한다. 신규 설치 스키마와 DB 마이그레이션을 추가했다. 임시 MySQL 테이블 회귀 테스트, PHP 문법 검사와 스키마 일치 검증을 통과했다. --- adm/shop_admin/admin.shop.lib.php | 3 +- adm/shop_admin/configform.php | 4 +- adm/shop_admin/orderformcartupdate.php | 9 +- adm/shop_admin/orderlistupdate.php | 5 + docs/order-completion-point.md | 23 ++++ install/gnuboard5shop.sql | 1 + lib/shop.lib.php | 22 +++- .../20260909_002_cart_complete_time.sql | 4 + tests/order_completion_point.php | 112 ++++++++++++++++++ 9 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 docs/order-completion-point.md create mode 100644 migrations/20260909_002_cart_complete_time.sql create mode 100644 tests/order_completion_point.php diff --git a/adm/shop_admin/admin.shop.lib.php b/adm/shop_admin/admin.shop.lib.php index b2be62547..ccd62bcc4 100644 --- a/adm/shop_admin/admin.shop.lib.php +++ b/adm/shop_admin/admin.shop.lib.php @@ -88,7 +88,8 @@ function change_status($od_id, $current_status, $change_status) $sql = " update {$g5['g5_shop_order_table']} set od_status = '{$change_status}' where od_id = '{$od_id}' and od_status = '{$current_status}' "; sql_query($sql, true); - $sql = " update {$g5['g5_shop_cart_table']} set ct_status = '{$change_status}' where od_id = '{$od_id}' and ct_status = '{$current_status}' "; + $complete_time_sql = get_cart_complete_time_sql($change_status); + $sql = " update {$g5['g5_shop_cart_table']} set ct_complete_time = $complete_time_sql, ct_status = '{$change_status}' where od_id = '{$od_id}' and ct_status = '{$current_status}' "; sql_query($sql, true); } diff --git a/adm/shop_admin/configform.php b/adm/shop_admin/configform.php index 53c001164..ee048e6fb 100644 --- a/adm/shop_admin/configform.php +++ b/adm/shop_admin/configform.php @@ -587,8 +587,8 @@ if( function_exists('pg_setting_check') ){ - - 주문 완료 일 이후에 포인트를 지급 + + 배송 완료 일 이후에 포인트를 지급 diff --git a/adm/shop_admin/orderformcartupdate.php b/adm/shop_admin/orderformcartupdate.php index 94ceee6ea..f005cfbcc 100644 --- a/adm/shop_admin/orderformcartupdate.php +++ b/adm/shop_admin/orderformcartupdate.php @@ -322,8 +322,10 @@ for ($i=0; $i<$cnt; $i++) $now = G5_TIME_YMDHIS; $ct_history="\n$ct_status|{$member['mb_id']}|$now|$REMOTE_ADDR"; + $complete_time_sql = get_cart_complete_time_sql($ct_status); $sql = " update {$g5['g5_shop_cart_table']} - set ct_point_use = '$point_use', + set ct_complete_time = $complete_time_sql, + ct_point_use = '$point_use', ct_stock_use = '$stock_use', ct_status = '$ct_status', ct_history = CONCAT(ct_history,'$ct_history') @@ -336,6 +338,11 @@ for ($i=0; $i<$cnt; $i++) $arr_it_id[] = $ct['it_id']; } +// 완료 처리 직후 실행하여 0일 설정도 즉시 적용한다. +if ($ct_status == '완료') { + save_order_point(); +} + // 상품 판매수량 반영 if(is_array($arr_it_id) && !empty($arr_it_id)) { $unq_it_id = array_unique($arr_it_id); diff --git a/adm/shop_admin/orderlistupdate.php b/adm/shop_admin/orderlistupdate.php index c7b6c89b4..19b1542a3 100644 --- a/adm/shop_admin/orderlistupdate.php +++ b/adm/shop_admin/orderlistupdate.php @@ -161,6 +161,11 @@ for ($i=0; $i<$count_post_chk; $i++) } +// 일괄 완료 처리도 0일 설정이면 즉시 적립한다. +if ($od_status == '완료') { + save_order_point(); +} + // SMS $sms_count = count($sms_messages); if($sms_count > 0) { diff --git a/docs/order-completion-point.md b/docs/order-completion-point.md new file mode 100644 index 000000000..40b595d4b --- /dev/null +++ b/docs/order-completion-point.md @@ -0,0 +1,23 @@ +# 배송완료 기준 주문 포인트 + +주문 포인트 지급 기간은 장바구니 상품·옵션 레코드(`ct_id`)별 배송완료 시각(`ct_complete_time`)부터 계산한다. 주문 접수 시각, 장바구니 생성 시각, 송장 등록 시각과는 구분한다. + +## 상태 변경과 지급 + +- 관리자 주문 상세의 선택 상품 완료 처리와 주문 목록의 일괄 완료 처리에서 완료 시각을 기록한다. +- 이미 완료된 상품을 다시 완료로 저장하면 기존 완료 시각을 유지한다. +- 완료를 해제하면 완료 시각을 `NULL`로 초기화한다. 이후 다시 완료하면 새 시각부터 지급 기간을 계산한다. +- 회원의 포인트가 있는 상품 중 완료 상태이고 아직 적립되지 않은 상품에 지급한다. +- 설정이 0일이면 완료 처리 시 지급한다. 그 외에는 설정 기간 경과 후 관리자 주문 상세 조회 또는 완료 처리 시 지급한다. 별도 예약 작업으로 정시에 지급하는 방식은 아니다. + +## 기존 설치 업그레이드 + +변경된 코드로 주문을 처리하기 전에 **환경설정 → DB업그레이드** 또는 CLI에서 `20260909_002_cart_complete_time` 마이그레이션을 적용한다. 선행 마이그레이션이 미적용 상태이면 먼저 적용해야 한다. + +기존 레코드는 완료 시각을 `NULL`로 유지한다. 기존 배송완료·미지급 상품은 지급을 추가로 늦추지 않도록 종전의 `ct_time` 기준을 적용한다. 완료 시각을 추정하거나 업그레이드 시각으로 덮어쓰지 않는다. 기존 상품도 완료를 해제한 후 다시 완료하면 새 기준을 적용한다. 이미 지급한 포인트는 마이그레이션으로 변경하지 않는다. + +신규 설치 스키마에도 같은 컬럼을 포함한다. 별도 확장 기능에서 상품 상태를 변경한다면 완료 시각 기록과 초기화도 함께 구현해야 한다. `get_cart_complete_time_sql()`의 반환식을 `ct_status` 대입보다 먼저 사용하면 기존 상태를 기준으로 완료 시각을 결정할 수 있다. + +## 검증 + +`php tests/order_completion_point.php`는 `data/dbconfig.php`의 DB 연결로 세션 전용 임시 테이블을 만들어 상태 변경, 기존 상품 전환 및 지급 조건을 검증한다. 실제 주문·포인트 테이블은 변경하지 않으며 포인트 등록 함수는 테스트 대역을 사용한다. DB 계정에 임시 테이블 생성 권한이 필요하다. diff --git a/install/gnuboard5shop.sql b/install/gnuboard5shop.sql index 680cde423..4fb827d5d 100644 --- a/install/gnuboard5shop.sql +++ b/install/gnuboard5shop.sql @@ -58,6 +58,7 @@ CREATE TABLE IF NOT EXISTS `g5_shop_cart` ( `ct_direct` tinyint(4) NOT NULL DEFAULT '0', `ct_select` tinyint(4) NOT NULL DEFAULT '0', `ct_select_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `ct_complete_time` datetime DEFAULT NULL, PRIMARY KEY (`ct_id`), KEY `od_id` (`od_id`), KEY `it_id` (`it_id`), diff --git a/lib/shop.lib.php b/lib/shop.lib.php index 92dbefcb5..a969c9d63 100644 --- a/lib/shop.lib.php +++ b/lib/shop.lib.php @@ -2500,17 +2500,33 @@ function exists_inicis_shop_order($oid, $pp=array(), $od_time='', $od_ip='') return ''; } +// 상태 변경 UPDATE에서 ct_status를 대입하기 전에 사용한다. +// 완료 상태에서 다시 저장하면 최초 완료 시각을 유지하고, 완료 해제 시 초기화한다. +function get_cart_complete_time_sql($ct_status) +{ + if ($ct_status != '완료') { + return 'NULL'; + } + + return "CASE WHEN ct_status = '완료' THEN ct_complete_time ELSE '".G5_TIME_YMDHIS."' END"; +} + //------------------------------------------------------------------------------ // 주문포인트를 적립한다. -// 설정일이 지난 포인트 부여되지 않은 배송완료된 장바구니 자료에 포인트 부여 -// 설정일이 0 이면 주문서 완료 설정 시점에서 포인트를 바로 부여합니다. +// 배송완료 시각(ct_complete_time)부터 설정일이 지난 미적립 상품에 포인트 부여 +// 설정일이 0 이면 배송완료 처리 시 포인트를 바로 부여합니다. //------------------------------------------------------------------------------ function save_order_point($ct_status="완료") { global $g5, $default; + // 완료 시각이 없는 기존 상품에만 종전의 장바구니 생성 시각 기준을 유지한다. $beforedays = date("Y-m-d H:i:s", ( time() - (86400 * (int)$default['de_point_days']) ) ); // 86400초는 하루 - $sql = " select * from {$g5['g5_shop_cart_table']} where ct_status = '$ct_status' and ct_point_use = '0' and ct_time <= '$beforedays' "; + $sql = " select * from {$g5['g5_shop_cart_table']} + where ct_status = '$ct_status' + and ct_point_use = '0' + and ((ct_complete_time is not null and ct_complete_time <= '$beforedays') + or (ct_complete_time is null and ct_time <= '$beforedays')) "; $result = sql_query($sql); for ($i=0; $row=sql_fetch_array($result); $i++) { // 회원 ID 를 얻는다. diff --git a/migrations/20260909_002_cart_complete_time.sql b/migrations/20260909_002_cart_complete_time.sql new file mode 100644 index 000000000..06d56ea34 --- /dev/null +++ b/migrations/20260909_002_cart_complete_time.sql @@ -0,0 +1,4 @@ +-- @description 상품별 배송완료 시각 추가 (기존 완료 상품은 종전 포인트 지급 기준 유지) +-- @if-table-exists {{g5_shop_cart_table}} +-- @if-column-missing {{g5_shop_cart_table}} ct_complete_time +ALTER TABLE `{{g5_shop_cart_table}}` ADD `ct_complete_time` datetime DEFAULT NULL AFTER `ct_select_time`; diff --git a/tests/order_completion_point.php b/tests/order_completion_point.php new file mode 100644 index 000000000..3595e6819 --- /dev/null +++ b/tests/order_completion_point.php @@ -0,0 +1,112 @@ +set_charset('utf8mb4'); +$g5['g5_shop_cart_table'] = 'test_completion_cart_'.getmypid(); +$g5['g5_shop_order_table'] = 'test_completion_order_'.getmypid(); +$cart = $g5['g5_shop_cart_table']; +$order = $g5['g5_shop_order_table']; +$points = array(); +function sql_query($sql, $error = true) { global $db; return $db->query($sql); } +function sql_fetch_array($result) { return $result->fetch_assoc(); } +function sql_fetch($sql) { return sql_fetch_array(sql_query($sql)); } +function insert_point($member, $amount, $content, $table, $id, $action) { + global $points; + $points[] = array($member, $amount, $action); +} +function expect_completion($condition, $message) { + if (!$condition) throw new Exception($message); +} +function completion_row($id) { + global $cart; + return sql_fetch("SELECT * FROM $cart WHERE ct_id = $id"); +} +function complete_item($id, $status) { + // 관리자 개별 처리의 실제 UPDATE 구문을 실행한다. + global $cart, $root, $g5; + $source = file_get_contents($root.'/adm/shop_admin/orderformcartupdate.php'); + $start = strpos($source, ' $complete_time_sql = get_cart_complete_time_sql($ct_status);'); + $end = strpos($source, ' sql_query($sql);', $start) + strlen(' sql_query($sql);'); + $ct_status = $status; + $ct_id = $id; + $ct = completion_row($id); + $od_id = $ct['od_id']; + $point_use = $ct['ct_point_use']; + $stock_use = 0; + $ct_history = ''; + eval(substr($source, $start, $end - $start)); +} +sql_query("CREATE TEMPORARY TABLE $order (od_id bigint PRIMARY KEY, mb_id varchar(255), od_status varchar(255))"); +sql_query("CREATE TEMPORARY TABLE $cart (ct_id int PRIMARY KEY, od_id bigint, ct_status varchar(255), ct_point_use int DEFAULT 0, ct_point int DEFAULT 10, ct_qty int DEFAULT 2, ct_stock_use int DEFAULT 0, ct_history text, ct_time datetime, ct_select_time datetime)"); +$old = date('Y-m-d H:i:s', time() - 86400 * 30); +$now = G5_TIME_YMDHIS; +sql_query("INSERT INTO $order VALUES (1, 'member', '배송'), (2, '', '완료')"); +sql_query("INSERT INTO $cart (ct_id, od_id, ct_status, ct_time, ct_select_time, ct_history) VALUES (1, 1, '완료', '$old', '$old', ''), (2, 1, '배송', '$old', '$old', '')"); +$migration = g5_migration_parse_file($root.'/migrations/20260909_002_cart_complete_time.sql'); +expect_completion(!isset($migration['error']), '마이그레이션 파싱'); +foreach ($migration['statements'] as $statement) { + sql_query(g5_migration_replace_placeholders($statement['sql'])); +} +expect_completion(completion_row(1)['ct_complete_time'] === null, '기존 완료 시각은 NULL 유지'); +complete_item(1, '완료'); +expect_completion(completion_row(1)['ct_complete_time'] === null, '기존 완료 재저장 시 종전 정책 유지'); +change_status(1, '배송', '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $now, '일괄 완료 시각 기록'); +expect_completion(completion_row(1)['ct_complete_time'] === null, '일괄 처리에서 기존 완료 상품 유지'); +complete_item(2, '배송'); +expect_completion(completion_row(2)['ct_complete_time'] === null, '개별 완료 해제'); +complete_item(2, '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $now, '개별 재완료 시각 기록'); +sql_query("UPDATE $cart SET ct_complete_time = '$old' WHERE ct_id = 2"); +complete_item(2, '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $old, '완료 재저장 시 최초 시각 유지'); +change_status(1, '완료', '배송'); +expect_completion(completion_row(2)['ct_complete_time'] === null, '일괄 완료 해제'); +change_status(1, '배송', '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $now, '일괄 재완료'); + +// 날짜 경계와 적립 루프를 실제 SQL로 검증한다. +sql_query("DELETE FROM $cart"); +$cutoff = date('Y-m-d H:i:s', time() - 86400 * 7); +$future = date('Y-m-d H:i:s', time() + 86400); +$rows = array( + array(1, 1, '완료', 0, $old, $now), // 오래된 장바구니, 오늘 완료 + array(2, 1, '완료', 0, $old, $cutoff), // 정확히 7일 경과 + array(3, 1, '완료', 0, $old, $old), + array(4, 1, '배송', 0, $old, $old), + array(5, 1, '취소', 0, $old, $old), + array(6, 1, '완료', 1, $old, $old), + array(7, 1, '완료', 0, $old, null), // 기존 완료 + array(8, 1, '완료', 0, $now, null), // 기존 기준에서도 대기 + array(9, 2, '완료', 0, $old, $old), // 비회원 + array(10, 1, '완료', 0, $old, $future) +); +foreach ($rows as $row) { + list($id, $od_id, $status, $used, $created, $completed) = $row; + $completed_sql = $completed === null ? 'NULL' : "'$completed'"; + sql_query("INSERT INTO $cart (ct_id, od_id, ct_status, ct_point_use, ct_time, ct_complete_time) VALUES ($id, $od_id, '$status', $used, '$created', $completed_sql)"); +} +$default = array('de_point_days' => 7); +save_order_point(); +expect_completion($points === array(array('member', 20, '1,2'), array('member', 20, '1,3'), array('member', 20, '1,7')), '배송완료 기간, 기존 정책, 상태, 회원, 수량 검증'); +save_order_point(); +expect_completion(count($points) === 3, '중복 지급 방지'); +$default['de_point_days'] = 0; +save_order_point(); +expect_completion(count($points) === 5, '0일이면 오늘 완료 및 기존 당일 상품 지급'); +expect_completion(completion_row(10)['ct_point_use'] == 0, '미래 완료 시각 제외'); +foreach (array('orderformcartupdate.php', 'orderlistupdate.php') as $file) { + $source = file_get_contents($root.'/adm/shop_admin/'.$file); + expect_completion(strpos($source, 'save_order_point();') !== false, $file.' 완료 후 적립 호출'); +} +echo "배송완료 시각 기록·유지·초기화, 부분 완료, 기존 상품 전환, 7일·0일 지급, 중복 지급 방지 통과\n";