security: KVE-2026-2424 LG 가상계좌 통보 보호와 5.6.40 배포 준비
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# 그누보드(영카트) 5.6.40 배포 안내
|
||||
|
||||
배포일: 2026-09-20. 변경 기준은 5.6.391입니다.
|
||||
|
||||
KVE-2026-2424 LG 가상계좌 입금 통보 위조를 차단하고 중복·역순 통보와 은행 취소 처리를 보완한 보안 업데이트입니다. **파일 반영 후 최고관리자 DB업그레이드가 필요합니다.**
|
||||
|
||||
## 변경 사항
|
||||
|
||||
- 통보 발신 IP·상점키·서명과 저장된 상점 MID·PG·거래번호·계좌·금액을 함께 검증합니다. 빈 키나 다른 PG 주문을 이용한 입금 처리를 차단합니다.
|
||||
- PC·모바일 일반 주문 및 개인결제에 결제 당시 LG MID를 저장합니다.
|
||||
- 통보 이력과 테이블 잠금으로 중복·동시·역순 통보를 처리하고, 중간 실패 시 같은 통보 재전송으로 복구할 수 있도록 합니다.
|
||||
- 은행 취소 시 확인된 입금액만 복원합니다. 개인결제 연결 주문이 입금 후 바뀌면 차감을 차단하며, 사용 중지된 개인결제도 기존 입금의 취소를 처리합니다.
|
||||
|
||||
## 업데이트 방법
|
||||
|
||||
1. 신규 주문·결제 접수를 잠시 중지하고 파일·DB를 백업합니다.
|
||||
2. 5.6.391 설치본에 전체 소스 또는 `gnuboard5.6.40.patch.zip`을 반영합니다. 사용자 수정 파일은 비교·병합합니다. 이번 패치의 삭제 대상 파일은 없습니다. 이전 버전은 선행 업데이트도 적용하십시오.
|
||||
3. 최고관리자의 **환경설정 → DB업그레이드**에서 `20260918_001_lg_notification`을 적용합니다. DB 계정에는 관련 테이블의 `LOCK TABLES`, `SELECT`, `INSERT`, `UPDATE` 권한이 필요합니다. 스키마 적용 전에는 주문 저장이나 통보 처리가 실패할 수 있습니다.
|
||||
4. 기존 미입금 LG 거래는 PG 원장과 대조한 뒤 확인된 건에만 `od_lg_mid`·`pp_lg_mid`를 보정하고 재통보를 요청합니다. 기존 행의 MID는 빈값으로 추가되며 이 상태에서는 통보를 거부합니다. 현재 설정을 과거 거래 전체에 일괄 복사하지 마십시오.
|
||||
5. HTTPS, 프록시의 원본 IP 복원과 실제 계약 PG 테스트 환경에서 PC·모바일 일반/개인결제의 계좌 발급·입금·은행 취소·재통보를 확인한 뒤 접수를 재개합니다.
|
||||
|
||||
일반형 가상계좌 전액 입금을 대상으로 합니다. 고정식 계좌·부분입금 및 계약별 취소 금액·순번 형식은 별도 대조가 필요합니다. 출고 후 은행 취소와 미완료 처리 이력은 운영자가 확인해야 합니다. 상세 적용·복구 절차와 검증 한계는 [KVE-2026-2424 안내](security-kve-2026-2424.md)를 참고하십시오.
|
||||
|
||||
## 검증
|
||||
|
||||
변경 PHP 8개 파일은 PHP 5.2.17·8.4.22 구문 검사를 통과했고, 마이그레이션 줄바꿈 13건을 확인했습니다. 격리된 MySQL의 MyISAM/InnoDB에서 실제 콜백과 SQL을 사용하는 HTTP·DB 회귀 검사 및 신규 설치 마이그레이션 이력 검사를 수행했습니다.
|
||||
|
||||
PG 발신 IP와 일부 실패 조건은 대역을 사용했습니다. 실제 외부 PG 통신·은행 입금, 전체 주문서 제출, 운영 서버와 모든 사용자 수정 환경의 검증을 포함하지 않습니다.
|
||||
@@ -0,0 +1,87 @@
|
||||
# KVE-2026-2424 LG 가상계좌 입금 통보 위조 차단
|
||||
|
||||
`shop/settle_lg_common.php`가 빈 LG 상점키로 만든 통보를 받아 다른 PG의 미입금 주문을 입금 처리할 수 있던 경로를 차단한다. PC·모바일의 가상계좌 통보는 이 공통 URL을 사용한다. 주문 처리 로직은 `lib/lg_notification.lib.php`로 분리했다.
|
||||
|
||||
## 적용 절차
|
||||
|
||||
1. 파일·DB를 백업하고 신규 주문·결제 접수를 일시 중지한다. 변경 파일 반영과 함께 최고관리자의 **환경설정 → DB업그레이드**에서 `20260918_001_lg_notification`을 적용한다. 새 설치 SQL에도 같은 컬럼과 이력 테이블이 포함된다. 일반 요청에서 DDL을 실행하지 않으며, 스키마 적용 전에는 새 주문 저장이나 통보 처리가 실패할 수 있다.
|
||||
2. DB 계정의 `LOCK TABLES` 및 관련 주문·개인결제·장바구니·통보 이력 테이블의 `SELECT`·`INSERT`·`UPDATE` 권한을 확인한다.
|
||||
3. **기존 미입금 LG 거래를 원장과 대조한다.** `od_lg_mid`·`pp_lg_mid`는 기존 행에 빈값으로 추가되며, 이 상태에서는 통보를 거부한다. 현재 설정을 과거 거래 전체에 일괄 복사하지 않는다. PG 거래 내역의 주문번호·거래번호·상점 MID·운영/테스트 여부·계좌·청구액·입금 여부를 확인한 건만 담당자가 해당 MID를 보정하고 PG 재통보를 요청한다. 운영 MID는 `si_` 접두어, 테스트 MID는 `tsi_` 접두어까지 포함한 실제 값이다. 이미 입금된 기존 거래에는 처리 이력을 임의로 만들지 말고 수동 대조한다.
|
||||
4. 웹서버에서 HTTPS 및 통보 발신지 처리를 확인한다. 애플리케이션은 `REMOTE_ADDR`만 신뢰한다. 프록시를 사용하는 경우 관리자가 신뢰 프록시 범위를 제한하고 원본 IP를 웹서버에서 복원해야 한다. `X-Forwarded-For` 등을 애플리케이션에서 무조건 신뢰하거나 프록시 IP를 PG IP 목록에 추가해서는 안 된다.
|
||||
5. 실제 계약된 XPay 테스트 환경에서 PC·모바일·일반/개인결제의 계좌 발급, 입금, 은행 입금 취소 및 재통보를 확인한 뒤 접수를 재개한다. 특히 계약별 취소 통보의 금액·순번 형식은 아래 지원 범위와 대조한다.
|
||||
6. 실패 통보, 미완료 처리 이력과 출고 후 은행 취소를 감시한다. 현재 설정의 MID/키가 과거 계약과 달라진 거래는 자동 처리하지 않는다. PG 전환만 있었고 LG 인증정보와 저장 거래를 검증할 수 있는 경우에는 기존 LG 거래를 처리한다.
|
||||
|
||||
신고된 위조 통보가 실제로 있었는지는 별도 확인 사항이다. 과거 LG 통보 기록과 입금 원장·주문 상태를 대조하고, 코드 적용만으로 과거 주문 정합성까지 복구됐다고 판단하지 않는다.
|
||||
|
||||
## 인증과 거래 검증
|
||||
|
||||
- 공식 XPay 공통사항 6.2의 운영·테스트 공통 통보 IP 10개에서 온 POST만 받는다. 임의의 클라이언트 IP, 관리자 세션, 전달 헤더는 인증 근거가 아니다.
|
||||
- 설정 MID·상점키가 없으면 거부한다. 요청 MID는 설정에서 계산한 `si_`/`tsi_` MID 중 하나여야 한다. 계약에서 확인되지 않은 임의의 키 길이 조건은 추가하지 않는다.
|
||||
- XPay 응답 규격의 MD5를 동일 길이 상수 시간 비교로 검증한다. MD5 입력에는 입금 플래그·거래번호·통보 순번이 포함되지 않으므로 **해시 검사만으로 전체 요청의 무결성이 보장된다고 간주하지 않는다.** PG 발신지와 서버 저장 거래 검증을 함께 적용한다.
|
||||
- 필수 입력의 문자열 여부·길이·허용 문자, 실제 달력 날짜와 시각, 양의 청구액 및 DB signed INT 범위를 검사한다. 배열·빈 TID·지수 표기·음수·0원·소수·범위 초과는 거부한다.
|
||||
- 결제 당시 서버에서 구성한 `$LGD_MID`를 PC·모바일 일반 주문과 개인결제에 저장한다. 현재 활성 PG나 테스트 설정으로 과거 주문을 판단하지 않는다. 통보 MID와 저장 MID, PG=`lg`, 결제수단=`가상계좌`, 거래번호·계좌를 대조하며 연결 주문의 테스트 여부도 검사한다.
|
||||
- 일반/개인결제 번호가 충돌하거나 연결 주문이 없으면 거부한다. 개인결제에 연결된 주문의 원래 PG는 다를 수 있으므로 개인결제 자체의 LG 거래를 검사한다.
|
||||
- 일반 입금은 서버의 상품·배송비·쿠폰·포인트·취소·환불 계산과 저장 미수금이 일치해야 한다. 미입금 주문의 청구액 전액만 반영한다. 개인결제는 `pp_price` 전액을 반영하며 연결 주문 미수금을 초과할 수 없다.
|
||||
|
||||
## 지원하는 통보와 재처리
|
||||
|
||||
기본 통합결제창의 **일반형 가상계좌 전액 입금**을 대상으로 한다. 고정식 계좌나 부분입금 기능을 새로 지원하는 패치는 아니다.
|
||||
|
||||
| 플래그 | 금액 검증 및 동작 |
|
||||
| --- | --- |
|
||||
| `R` | 누적/현입금액 0. 저장된 거래와 대조한 뒤 수신만 확인하며 입금액을 바꾸지 않는다. |
|
||||
| `I` | `LGD_AMOUNT`, `LGD_CASTAMOUNT`, `LGD_CASCAMOUNT`가 청구액 전액과 같아야 한다. 해당 거래가 미입금 상태인 경우에만 반영한다. |
|
||||
| `C` | 누적액 0, 현입금액 0 또는 청구액 전액인 은행 취소를 처리한다. 확인된 입금 이력이 있으면 해당 금액만 차감한다. 입금보다 먼저 도착하면 취소 이력을 남겨 지연 입금 통보를 차단한다. |
|
||||
|
||||
통보 이력은 MID·TID·주문번호·숫자로 정규화한 `LGD_CASSEQNO`·플래그로 구별한다. 같은 이벤트의 금액·계좌 등 업무 값이 다르면 거부한다. 완료된 이벤트 재전송은 금액을 다시 반영하지 않고 `OK`로 응답한다. 타임스탬프의 임의 만료 시간으로 수동 재통보를 막지 않는다.
|
||||
|
||||
순번은 입금 순서를 나타내는 숫자(최대 6자리)로 취급한다. 같은 순번의 입금보다 취소가 우선하며, 더 낮은 순번의 지연 통보는 업무 데이터를 변경하지 않는다. 취소 후 새 입금은 증가한 순번에서만 허용한다. 계약 환경이 순번을 재사용하거나 다른 취소 금액 형식을 사용하면 원장과 대조하여 연동 정책을 확정해야 하며, 검증을 제거해 우회하지 않는다. 실제 PG 통보의 모든 변형을 확인한 것은 아니다.
|
||||
|
||||
은행 취소 시 입금액·미수금을 복원하고 `입금`·`준비` 상태를 `주문`으로 되돌린다. 이미 `배송`·`완료`인 경우 물류 이력은 유지하고 미수금 및 운영자 대조 필요 로그를 남긴다. 출고 중단·회수·디지털 상품 접근·포인트 회수는 운영자가 별도로 조치해야 한다. 이력 없이 입금액이 남아 있는 과거 거래, 취소·반품 주문, 부분입금과 불일치 금액은 자동으로 추정·차감하지 않는다.
|
||||
|
||||
개인결제의 은행 취소는 활성 입금 이력에 기록된 연결 주문과 현재 연결 주문을 대조한다. 입금 후 연결 주문 변경·해제·추가가 있으면 금액과 이력을 변경하지 않고 `FAIL` 및 연결 충돌 로그를 남긴다. 다른 주문에서 대신 차감하지 않는다. 담당자는 PG 원장과 입금 이력을 대조하고 원래 연결 관계를 복원한 뒤 동일 취소 통보를 재전송한다. 개인결제를 `사용 안 함`으로 바꿔도 기존 입금의 은행 취소는 처리하며, 사용 중지 상태의 신규 입금은 계속 거부한다.
|
||||
|
||||
## 중간 실패와 복구
|
||||
|
||||
MyISAM·InnoDB 모두 관련 네 테이블의 쓰기 잠금 안에서 검증·처리한다. `ln_plan`에 행별 변경 전 값과 목표값을 기록한 다음 개인결제·장바구니·주문을 갱신한다. 재시도에서는 가산 연산 대신 같은 목표값을 저장하므로 개인결제의 연결 주문 금액이 중복 가산되지 않는다. 모든 결과를 다시 읽어 확인하고 `ln_done=1`로 기록해야 `OK`로 응답한다. 실패 응답은 `FAIL`이다.
|
||||
|
||||
이 방식이 MyISAM 여러 테이블을 원자적 트랜잭션으로 바꾸지는 않는다. 중간 실패 시 일부 값이 반영될 수 있고, 같은 통보 재전송으로 나머지를 완료한다. 현재 행이 기록된 변경 전/후 어느 쪽과도 맞지 않으면 외부 변경으로 보고 복구를 중단한다. 미완료 이벤트가 남은 거래 및 연결 주문의 다른 신규 LG 통보도 차단한다. 다른 PG나 관리자 작업에서 발생한 충돌은 원장 대조가 필요하다. 테이블 전체 잠금 동안 주문 및 관리자 쿼리가 잠시 대기할 수 있다.
|
||||
|
||||
복구 점검 SQL(실제 설치의 테이블 접두어로 변경):
|
||||
|
||||
```sql
|
||||
SELECT ln_key, ln_trade, ln_seq, ln_flag, od_id, ln_created_at
|
||||
FROM g5_shop_lg_noti WHERE ln_done = 0 ORDER BY ln_created_at;
|
||||
```
|
||||
|
||||
미완료 이력을 삭제하거나 `ln_done`만 1로 변경하지 않는다. PG 원장과 주문·개인결제·장바구니·`ln_plan`을 대조하고 충돌 원인을 해결한 뒤 동일 통보를 재전송한다. 공식 가이드는 자동 재전송 후에도 처리되지 않은 거래를 구 상점관리자의 전송실패내역에서 재전송할 수 있다고 안내한다. 이력에는 계좌·금액 등 거래 정보가 포함되므로 주문 데이터와 같은 접근·백업 보호를 적용한다.
|
||||
|
||||
## 검증
|
||||
|
||||
```sh
|
||||
python3 tests/lg_notification_http.py 격리_MySQL_포트
|
||||
php tests/migration_line_endings.php
|
||||
php tests/migration_install_history.php 격리_MySQL_소켓
|
||||
```
|
||||
|
||||
HTTP 검사는 localhost 지정 포트의 root/빈 비밀번호인 **별도 테스트 MySQL**에 고유 임시 DB를 만들고 삭제한다. 실제 콜백 파일과 SQL을 실행하되 공통 초기화·PG 발신 IP·SQL 실패 주입만 대역으로 사용한다. Python 3, PHP CLI/mysqli, MySQL CLI가 필요하다. 테스트 파일은 일반 배포 아카이브에서 제외된다.
|
||||
|
||||
2026-09-18: PHP 8.4.22·MySQL 8.0.46의 MyISAM/InnoDB에서 HTTP·DB 645건 통과. 빈 키 제보 조건, 서명/IP/상점/거래/금액 거부, 일반·개인결제, 동시·중복·역순 통보, 은행 취소, 각 쓰기 전 실패·반영 후 응답 유실·실제 SQL 오류와 복구, 외부 수정 충돌, 스키마 누락, 새 설치/기존 업그레이드·재실행·쇼핑몰 미설치 분기를 확인했다. 마이그레이션 줄바꿈 13건 및 기존 신규 설치 이력·체크섬·재실행 검사가 통과했다. 변경 PHP 8개 파일의 구문 검사는 PHP 5.2.17·8.4.22에서 통과했다.
|
||||
|
||||
실제 PG 승인·은행 입금, 전체 주문서 제출, 프록시 설정, MySQL 5.0 실행, 실제 운영 배포는 이 격리 검증에 포함하지 않는다. 자동 거래조회 API는 이번 패치에 추가하지 않았다. 공식 IP·서명과 저장 거래를 함께 검증하고, 불명확한 기존 거래는 원장 대조로 처리한다.
|
||||
|
||||
2026-09-18 추가 보완 검증: PHP 8.4.25·별도 localhost MySQL 8.0.46에서 MyISAM/InnoDB 회귀 검사 715건과 실제 설치의 공통 초기화·DB 함수를 사용하는 검사 391건을 통과했다. 입금 후 개인결제 연결 주문 변경·해제·추가의 차단 및 원래 연결 복원 후 재통보, 사용 중지 후 동시 은행 취소·중복 차감 방지·신규 입금 거부를 추가했다. Windows 실행에 맞춰 테스트 사본의 경로·프로세스 종료를 조정하고 PHP 서버 4개로 동시 요청을 처리했다. 실제 초기화 검사에서도 PG 발신 IP는 로컬 라우터로 대입했으며 실제 PG 통신을 수행한 것은 아니다. 두 검사 집합의 시나리오는 중복되며, 이번 보완은 추가 DB 마이그레이션을 요구하지 않는다.
|
||||
|
||||
## 배포 및 인터넷진흥원 회신
|
||||
|
||||
현재는 코드 수정과 격리 검증을 완료한 상태이다. 공개 배포 버전·일정, 실제 계약 PG 연동 검사, 운영 반영과 KISA 제출은 별도 기록한다.
|
||||
|
||||
> KVE-2026-2424와 관련하여 미설정 LG 인증정보 차단, 통보 발신지·서명·저장 거래·결제금액 검증을 보완하였습니다. 일반 주문과 개인결제에 중복 통보 방지 및 은행 입금 취소 처리를 적용하고, DB 처리 중 실패한 건을 동일 통보로 복구하도록 수정하였습니다. 격리 환경에서 위조 요청 차단, 동시·역순 통보, 중간 실패 복구 및 신규/기존 DB 적용을 검증하였습니다. 기존 미입금 거래는 PG 원장 대조 후 결제 당시 MID를 확인해야 하며, 실제 PG 연동 검증과 공개 배포 버전·일정은 확정 후 별도로 통보하겠습니다.
|
||||
|
||||
## 규격 근거
|
||||
|
||||
2026-09-18에 공식 [구 모듈 가이드](https://docs.tosspayments.com/legacy/guide)에서 연결되는 다음 문서를 확인했다.
|
||||
|
||||
- [XPay 결제모듈 공통사항 v1.0.8](https://docs.google.com/document/d/1kyhcyXmaRuxZAlNJ_Oiuo2EoFVMlQbHZewZW40E6u2w/edit): 6.2의 가상계좌 입금 통보 발신지. [현행 방화벽 안내](https://docs.tosspayments.com/reference/using-api/security)의 인바운드 IP와도 일치한다.
|
||||
- [PC 통합결제창 가상계좌 v1.0.3](https://docs.google.com/document/d/1YZ5AuUVFlxRZriWkq3YlMrjPu6iEJSZFbI2KcNijdac/edit): 일반형 계좌의 일회성 입금, 발급/입금/은행 취소 구분, 통보 응답 및 재전송 안내.
|
||||
- 저장소의 `plugin/lgxpay/lgdacom/XPayClient.php::GetHashData2()`와 기존 콜백의 응답 해시 계산식도 대조했다. 키 길이·자동 조회 API를 임의로 가정하지 않았다.
|
||||
@@ -684,6 +684,7 @@ CREATE TABLE IF NOT EXISTS `g5_shop_order` (
|
||||
`od_mobile` tinyint(4) NOT NULL DEFAULT '0',
|
||||
`od_pg` varchar(255) NOT NULL DEFAULT '',
|
||||
`od_kcp_site_cd` varchar(5) NOT NULL DEFAULT '',
|
||||
`od_lg_mid` varchar(104) NOT NULL DEFAULT '',
|
||||
`od_tno` varchar(255) NOT NULL DEFAULT '',
|
||||
`od_app_no` varchar(20) NOT NULL DEFAULT '',
|
||||
`od_escrow` tinyint(4) NOT NULL DEFAULT '0',
|
||||
@@ -783,6 +784,7 @@ CREATE TABLE IF NOT EXISTS `g5_shop_personalpay` (
|
||||
`pp_price` INT(11) NOT NULL DEFAULT '0',
|
||||
`pp_pg` varchar(255) NOT NULL DEFAULT '',
|
||||
`pp_kcp_site_cd` varchar(5) NOT NULL DEFAULT '',
|
||||
`pp_lg_mid` varchar(104) NOT NULL DEFAULT '',
|
||||
`pp_tno` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`pp_app_no` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`pp_casseqno` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
@@ -1071,3 +1073,19 @@ CREATE TABLE `g5_shop_order_access` (
|
||||
KEY cart_status (cart_id,status),
|
||||
KEY state_expiry (status,expires)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- LG 입금 통보 및 중간 실패 복구 이력
|
||||
CREATE TABLE `g5_shop_lg_noti` (
|
||||
`ln_key` char(64) NOT NULL,
|
||||
`ln_trade` char(64) NOT NULL,
|
||||
`ln_seq` int(11) NOT NULL,
|
||||
`ln_flag` char(1) NOT NULL,
|
||||
`ln_payload` char(64) NOT NULL,
|
||||
`od_id` bigint(20) unsigned NOT NULL DEFAULT '0',
|
||||
`ln_plan` mediumtext NOT NULL,
|
||||
`ln_done` tinyint(4) NOT NULL DEFAULT '0',
|
||||
`ln_created_at` datetime NOT NULL,
|
||||
PRIMARY KEY (`ln_key`),
|
||||
KEY `ln_trade` (`ln_trade`),
|
||||
KEY `od_id` (`od_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||
|
||||
@@ -678,6 +678,7 @@ if($g5_shop_install) {
|
||||
install_file_write($f, "\$g5['g5_shop_sendcost_table'] = G5_SHOP_TABLE_PREFIX.'sendcost'; // 추가배송비 테이블\n");
|
||||
install_file_write($f, "\$g5['g5_shop_personalpay_table'] = G5_SHOP_TABLE_PREFIX.'personalpay'; // 개인결제 정보 테이블\n");
|
||||
install_file_write($f, "\$g5['g5_shop_kcp_noti_table'] = G5_SHOP_TABLE_PREFIX.'kcp_noti'; // KCP 통보 처리 이력 테이블\n");
|
||||
install_file_write($f, "\$g5['g5_shop_lg_noti_table'] = G5_SHOP_TABLE_PREFIX.'lg_noti'; // LG 통보 처리 이력 테이블\n");
|
||||
install_file_write($f, "\$g5['g5_shop_order_address_table'] = G5_SHOP_TABLE_PREFIX.'order_address'; // 배송지이력 정보 테이블\n");
|
||||
install_file_write($f, "\$g5['g5_shop_item_stocksms_table'] = G5_SHOP_TABLE_PREFIX.'item_stocksms'; // 재입고SMS 알림 정보 테이블\n");
|
||||
install_file_write($f, "\$g5['g5_shop_post_log_table'] = G5_SHOP_TABLE_PREFIX.'order_post_log'; // 주문요청 로그 테이블\n");
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
include_once(dirname(__FILE__).'/shop_order_compat.lib.php');
|
||||
|
||||
function lg_noti_query($sql)
|
||||
{
|
||||
$result = sql_query($sql, false);
|
||||
if (!$result) {
|
||||
throw new Exception('LG notification database failure');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function lg_noti_rows($sql)
|
||||
{
|
||||
$result = lg_noti_query($sql);
|
||||
$rows = array();
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function lg_noti_quote($value)
|
||||
{
|
||||
return "'".sql_escape_string((string) $value)."'";
|
||||
}
|
||||
|
||||
function lg_noti_tables()
|
||||
{
|
||||
global $g5;
|
||||
return array('order' => $g5['g5_shop_order_table'], 'personal' => $g5['g5_shop_personalpay_table'],
|
||||
'cart' => $g5['g5_shop_cart_table'],
|
||||
'event' => isset($g5['g5_shop_lg_noti_table']) ? $g5['g5_shop_lg_noti_table'] : G5_SHOP_TABLE_PREFIX.'lg_noti');
|
||||
}
|
||||
|
||||
function lg_noti_process($data)
|
||||
{
|
||||
$tables = lg_noti_tables();
|
||||
$locked = false;
|
||||
try {
|
||||
// 기본 설치의 MyISAM에서도 다른 통보 및 관리자 UPDATE와 경쟁하지 않게 한다.
|
||||
// DDL은 관리자 DB 업그레이드에서만 실행하며 스키마/권한이 없으면 실패 응답한다.
|
||||
$locks = array();
|
||||
foreach ($tables as $table) {
|
||||
$locks[] = '`'.$table.'` WRITE';
|
||||
}
|
||||
lg_noti_query('LOCK TABLES '.implode(', ', $locks));
|
||||
$locked = true;
|
||||
$success = lg_noti_locked($data, $tables);
|
||||
} catch (Exception $e) {
|
||||
error_log('LG notification: processing failed; reconciliation/retry required');
|
||||
$success = false;
|
||||
} catch (Throwable $e) {
|
||||
error_log('LG notification: processing failed; reconciliation/retry required');
|
||||
$success = false;
|
||||
}
|
||||
if ($locked) {
|
||||
try {
|
||||
lg_noti_query('UNLOCK TABLES');
|
||||
} catch (Exception $e) {
|
||||
$success = false;
|
||||
} catch (Throwable $e) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
// 변경 전/후 값을 함께 기록한다. 재시도는 가산 SQL 대신 목표값을 대입한다.
|
||||
function lg_noti_step($table, $id_field, $row, $changes, $guards)
|
||||
{
|
||||
$before = array();
|
||||
foreach (array_unique(array_merge(array($id_field), array_keys($changes), $guards)) as $field) {
|
||||
if (!array_key_exists($field, $row)) {
|
||||
throw new Exception('Missing notification schema');
|
||||
}
|
||||
$before[$field] = (string) $row[$field];
|
||||
}
|
||||
$after = $before;
|
||||
foreach ($changes as $field => $value) {
|
||||
$after[$field] = (string) $value;
|
||||
}
|
||||
return array('table' => $table, 'id_field' => $id_field, 'id' => (string) $row[$id_field],
|
||||
'before' => $before, 'after' => $after);
|
||||
}
|
||||
|
||||
function lg_noti_matches($row, $expected)
|
||||
{
|
||||
foreach ($expected as $field => $value) {
|
||||
if (!array_key_exists($field, $row) || (string) $row[$field] !== $value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function lg_noti_apply($plan, $tables)
|
||||
{
|
||||
// 먼저 모든 행을 검사한다. 실패 후 외부에서 수정한 행을 과거 값으로 덮어쓰지 않는다.
|
||||
foreach ($plan as $step) {
|
||||
$rows = lg_noti_rows('SELECT * FROM `'.$tables[$step['table']].'` WHERE `'.$step['id_field'].'` = '.lg_noti_quote($step['id']));
|
||||
if (count($rows) !== 1 || (!lg_noti_matches($rows[0], $step['before']) && !lg_noti_matches($rows[0], $step['after']))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
foreach ($plan as $step) {
|
||||
$sets = array();
|
||||
foreach ($step['after'] as $field => $value) {
|
||||
if ($value !== $step['before'][$field]) {
|
||||
$sets[] = '`'.$field.'` = '.lg_noti_quote($value);
|
||||
}
|
||||
}
|
||||
if ($sets) {
|
||||
$sql = 'UPDATE `'.$tables[$step['table']].'` SET '.implode(', ', $sets)
|
||||
.' WHERE `'.$step['id_field'].'` = '.lg_noti_quote($step['id']);
|
||||
lg_noti_query($sql);
|
||||
}
|
||||
}
|
||||
foreach ($plan as $step) {
|
||||
$rows = lg_noti_rows('SELECT * FROM `'.$tables[$step['table']].'` WHERE `'.$step['id_field'].'` = '.lg_noti_quote($step['id']));
|
||||
if (count($rows) !== 1 || !lg_noti_matches($rows[0], $step['after'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// XPay 공통사항 6.2 및 가상계좌 가이드(2026-09-18 확인).
|
||||
// MD5에는 CASFLAG/TID/순번이 포함되지 않으므로 발신지 검사도 반드시 적용한다.
|
||||
function lg_noti_request($post, $server, $config)
|
||||
{
|
||||
$ips = array('13.124.18.147', '13.124.108.35', '3.36.173.151', '3.38.81.32',
|
||||
'115.92.221.121', '115.92.221.122', '115.92.221.123', '115.92.221.125', '115.92.221.126', '115.92.221.127');
|
||||
if (!isset($server['REQUEST_METHOD'], $server['REMOTE_ADDR']) || $server['REQUEST_METHOD'] !== 'POST'
|
||||
|| !in_array($server['REMOTE_ADDR'], $ips, true)) return false;
|
||||
// 프록시 전달 헤더, 현재 로그인/활성 PG 설정은 통보 인증에 사용하지 않는다.
|
||||
if (!isset($config['cf_lg_mid'], $config['cf_lg_mert_key'])
|
||||
|| !is_string($config['cf_lg_mid']) || !preg_match('/\A[A-Za-z0-9_-]{1,100}\z/', $config['cf_lg_mid'])
|
||||
|| !is_string($config['cf_lg_mert_key']) || trim($config['cf_lg_mert_key']) === '') return false;
|
||||
$patterns = array(
|
||||
'LGD_MID' => '/\A[A-Za-z0-9_-]{1,104}\z/', 'LGD_OID' => '/\A[1-9][0-9]{0,19}\z/',
|
||||
'LGD_TID' => '/\A[A-Za-z0-9_-]{1,64}\z/', 'LGD_RESPCODE' => '/\A0000\z/',
|
||||
'LGD_PAYTYPE' => '/\ASC0040\z/', 'LGD_HASHDATA' => '/\A[a-fA-F0-9]{32}\z/',
|
||||
'LGD_TIMESTAMP' => '/\A[0-9]{14}\z/', 'LGD_PAYDATE' => '/\A[0-9]{14}\z/',
|
||||
'LGD_AMOUNT' => '/\A[0-9]{1,12}\z/', 'LGD_CASTAMOUNT' => '/\A[0-9]{1,12}\z/',
|
||||
'LGD_CASCAMOUNT' => '/\A[0-9]{1,12}\z/', 'LGD_CASFLAG' => '/\A[RIC]\z/',
|
||||
'LGD_CASSEQNO' => '/\A[0-9]{1,6}\z/', 'LGD_ACCOUNTNUM' => '/\A[A-Za-z0-9-]{1,30}\z/'
|
||||
);
|
||||
$data = array();
|
||||
foreach ($patterns as $field => $pattern) {
|
||||
if (!isset($post[$field]) || !is_string($post[$field]) || !preg_match($pattern, $post[$field])) return false;
|
||||
$data[$field] = $post[$field];
|
||||
}
|
||||
$mid = 'si_'.$config['cf_lg_mid'];
|
||||
if (!shop_order_equals($mid, $data['LGD_MID']) && !shop_order_equals('t'.$mid, $data['LGD_MID'])) return false;
|
||||
$hash = md5($data['LGD_MID'].$data['LGD_OID'].$data['LGD_AMOUNT'].$data['LGD_RESPCODE'].$data['LGD_TIMESTAMP'].$config['cf_lg_mert_key']);
|
||||
if (!shop_order_equals($hash, strtolower($data['LGD_HASHDATA']))) return false;
|
||||
foreach (array('LGD_TIMESTAMP', 'LGD_PAYDATE') as $field) {
|
||||
$time = $data[$field];
|
||||
if ((int)substr($time, 0, 4) < 1000 || !checkdate((int)substr($time, 4, 2), (int)substr($time, 6, 2), (int)substr($time, 0, 4))
|
||||
|| substr($time, 8, 2) > 23 || substr($time, 10, 2) > 59 || substr($time, 12, 2) > 59) return false;
|
||||
}
|
||||
// 수동 재통보를 막는 임의의 timestamp 수명 제한 대신 거래별 처리 이력을 확인한다.
|
||||
foreach (array('LGD_AMOUNT', 'LGD_CASTAMOUNT', 'LGD_CASCAMOUNT') as $field) {
|
||||
if ((float)$data[$field] > 2147483647) return false;
|
||||
$data[$field] = (int)$data[$field];
|
||||
}
|
||||
if ($data['LGD_AMOUNT'] <= 0) return false;
|
||||
$flag = $data['LGD_CASFLAG'];
|
||||
if ($flag === 'R') {
|
||||
if ($data['LGD_CASTAMOUNT'] !== 0 || $data['LGD_CASCAMOUNT'] !== 0) return false;
|
||||
} else {
|
||||
if ((int)$data['LGD_CASSEQNO'] === 0) return false;
|
||||
// 기본 통합결제창의 일반형 가상계좌는 전액 일회성 입금이다.
|
||||
if ($flag === 'I' && ($data['LGD_CASTAMOUNT'] !== $data['LGD_AMOUNT'] || $data['LGD_CASCAMOUNT'] !== $data['LGD_AMOUNT'])) return false;
|
||||
if ($flag === 'C' && ($data['LGD_CASTAMOUNT'] !== 0 || !in_array($data['LGD_CASCAMOUNT'], array(0, $data['LGD_AMOUNT']), true))) return false;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
function lg_noti_locked($data, $tables)
|
||||
{
|
||||
$id = lg_noti_quote($data['LGD_OID']);
|
||||
$personal = lg_noti_rows("SELECT * FROM `{$tables['personal']}` WHERE pp_id = $id");
|
||||
$orders = lg_noti_rows("SELECT * FROM `{$tables['order']}` WHERE od_id = $id");
|
||||
if (count($personal) > 1 || count($orders) > 1 || ($personal && $orders)) return false;
|
||||
$pp = $personal ? $personal[0] : null;
|
||||
if ($pp) {
|
||||
$prefix = 'pp'; $payment = $pp;
|
||||
$orders = $pp['od_id'] ? lg_noti_rows("SELECT * FROM `{$tables['order']}` WHERE od_id = ".lg_noti_quote($pp['od_id'])) : array();
|
||||
if ($pp['od_id'] && count($orders) !== 1) return false;
|
||||
} else {
|
||||
if (!$orders) return false;
|
||||
$prefix = 'od'; $payment = $orders[0];
|
||||
}
|
||||
$od = $orders ? $orders[0] : null;
|
||||
if ($payment[$prefix.'_pg'] !== 'lg' || $payment[$prefix.'_settle_case'] !== '가상계좌'
|
||||
|| $payment[$prefix.'_tno'] !== $data['LGD_TID'] || empty($payment[$prefix.'_lg_mid'])
|
||||
|| !shop_order_equals($payment[$prefix.'_lg_mid'], $data['LGD_MID'])) return false;
|
||||
if ($od && (bool)$od['od_test'] !== (substr($data['LGD_MID'], 0, 1) === 't')) return false;
|
||||
// 저장 형식: 은행명 계좌번호 예금주명. 부분 문자열 일치는 허용하지 않는다.
|
||||
if (!in_array($data['LGD_ACCOUNTNUM'], preg_split('/\s+/', trim($payment[$prefix.'_bank_account'])), true)) return false;
|
||||
if ($data['LGD_CASFLAG'] === 'R') return true; // 발급 통보로 입금 처리하지 않는다.
|
||||
return lg_noti_deposit($data, $tables, $payment, $prefix, $pp, $od);
|
||||
}
|
||||
|
||||
function lg_noti_deposit($data, $tables, $payment, $prefix, $pp, $od)
|
||||
{
|
||||
$trade = hash('sha256', $data['LGD_MID'].'|'.$data['LGD_TID'].'|'.$data['LGD_OID']);
|
||||
$seq = (int)$data['LGD_CASSEQNO'];
|
||||
$flag = $data['LGD_CASFLAG'];
|
||||
$key = hash('sha256', $trade.'|'.$seq.'|'.$flag);
|
||||
// 재전송 timestamp는 달라질 수 있다. 업무 값을 바꾼 동일 이벤트는 거부한다.
|
||||
$payload = hash('sha256', $data['LGD_AMOUNT'].'|'.$data['LGD_CASTAMOUNT'].'|'.$data['LGD_CASCAMOUNT'].'|'.$data['LGD_ACCOUNTNUM']);
|
||||
$events = lg_noti_rows("SELECT * FROM `{$tables['event']}` WHERE ln_trade = '$trade'");
|
||||
$same = null; $latest = null;
|
||||
foreach ($events as $event) {
|
||||
if ($event['ln_key'] === $key) $same = $event;
|
||||
if (!$latest || (int)$event['ln_seq'] > (int)$latest['ln_seq']
|
||||
|| ((int)$event['ln_seq'] === (int)$latest['ln_seq'] && $event['ln_flag'] === 'C')) $latest = $event;
|
||||
if (!$event['ln_done'] && $event['ln_key'] !== $key) return false;
|
||||
}
|
||||
if ($same) {
|
||||
if ($same['ln_payload'] !== $payload) return false;
|
||||
if ($same['ln_done']) return true;
|
||||
$plan = json_decode($same['ln_plan'], true);
|
||||
if (!is_array($plan) || !lg_noti_apply($plan, $tables)) return false;
|
||||
lg_noti_query("UPDATE `{$tables['event']}` SET ln_done = 1 WHERE ln_key = '$key'");
|
||||
return true;
|
||||
}
|
||||
// 은행 취소가 먼저 와도 같은/과거 입금 순번으로 다시 입금시키지 않는다.
|
||||
if ($latest && ($seq < (int)$latest['ln_seq'] || ($seq === (int)$latest['ln_seq'] && $latest['ln_flag'] === 'C'))) return true;
|
||||
$canceling = $flag === 'C';
|
||||
// 사용 중지는 신규 입금만 막는다. 이미 수납한 거래의 은행 취소는 반영해야 한다.
|
||||
if ($pp && !$pp['pp_use'] && !$canceling) return false;
|
||||
$amount = $data['LGD_AMOUNT'];
|
||||
$receipt = (int)$payment[$prefix.'_receipt_price'];
|
||||
$active = $latest && $latest['ln_flag'] === 'I';
|
||||
if ($pp && (int)$pp['pp_price'] !== $amount) return false;
|
||||
if (!$canceling && ($active || $receipt !== 0)) return false;
|
||||
// 이력 없는 기존 입금은 자동으로 취소하거나 기처리로 추정하지 않는다.
|
||||
if ($canceling && $receipt !== ($active ? $amount : 0)) return false;
|
||||
$delta = $canceling ? ($active ? -$amount : 0) : $amount;
|
||||
$od_id = $od ? $od['od_id'] : '0';
|
||||
// 관리자가 연결 주문을 바꿔도 이전 입금의 취소액을 새 주문에서 차감하지 않는다.
|
||||
// 연결 해제/추가도 충돌이며 원장 대조 후 원래 연결을 복원하고 재통보한다.
|
||||
if ($pp && $canceling && $active && (string)$latest['od_id'] !== (string)$od_id) {
|
||||
error_log('LG notification: personal payment order binding changed; reconciliation required');
|
||||
return false;
|
||||
}
|
||||
if ($od) {
|
||||
$pending = lg_noti_rows("SELECT ln_key FROM `{$tables['event']}` WHERE od_id = ".lg_noti_quote($od_id)." AND ln_done = 0");
|
||||
if ($pending) return false;
|
||||
$due = (int)$od['od_cart_price'] + (int)$od['od_send_cost'] + (int)$od['od_send_cost2']
|
||||
- (int)$od['od_cart_coupon'] - (int)$od['od_coupon'] - (int)$od['od_send_coupon']
|
||||
- (int)$od['od_cancel_price'] - (int)$od['od_receipt_point'] + (int)$od['od_refund_price'];
|
||||
$misu = $due - (int)$od['od_receipt_price'];
|
||||
if ($due <= 0 || $due > 2147483647 || $misu !== (int)$od['od_misu']) return false;
|
||||
if (!$pp && ($due !== $amount || (int)$od['od_cancel_price'] !== 0 || (int)$od['od_refund_price'] !== 0)) return false;
|
||||
if (!$canceling && ($od['od_status'] !== '주문' || $misu < $amount)) return false;
|
||||
if ($canceling && (!in_array($od['od_status'], array('주문', '입금', '준비', '배송', '완료'), true)
|
||||
|| (int)$od['od_receipt_price'] + $delta < 0)) return false;
|
||||
}
|
||||
$raw_time = $data['LGD_PAYDATE'];
|
||||
$time = substr($raw_time, 0, 4).'-'.substr($raw_time, 4, 2).'-'.substr($raw_time, 6, 2)
|
||||
.' '.substr($raw_time, 8, 2).':'.substr($raw_time, 10, 2).':'.substr($raw_time, 12, 2);
|
||||
$plan = array();
|
||||
if ($pp && $delta) {
|
||||
$plan[] = lg_noti_step('personal', 'pp_id', $pp,
|
||||
array('pp_receipt_price'=>$receipt + $delta, 'pp_receipt_time'=>$time, 'pp_casseqno'=>$data['LGD_CASSEQNO']),
|
||||
array('od_id', 'pp_pg', 'pp_tno', 'pp_lg_mid', 'pp_settle_case', 'pp_price', 'pp_use', 'pp_bank_account'));
|
||||
}
|
||||
if ($od && $delta) {
|
||||
$new_receipt = (int)$od['od_receipt_price'] + $delta;
|
||||
$new_misu = $due - $new_receipt;
|
||||
if ($new_receipt < 0 || $new_receipt > 2147483647 || $new_misu < 0 || $new_misu > 2147483647) return false;
|
||||
$status = $od['od_status'];
|
||||
if (!$canceling && $new_misu === 0) $status = '입금';
|
||||
if ($canceling && in_array($status, array('입금', '준비'), true)) $status = '주문';
|
||||
$cart = lg_noti_rows("SELECT ct_id, od_id, ct_status FROM `{$tables['cart']}` WHERE od_id = ".lg_noti_quote($od_id));
|
||||
if (!$cart) return false;
|
||||
foreach ($cart as $item) {
|
||||
if (!$canceling && $item['ct_status'] !== '주문') return false;
|
||||
if ($status !== $od['od_status'] && ($item['ct_status'] === $od['od_status']
|
||||
|| ($canceling && in_array($item['ct_status'], array('입금', '준비'), true)))) {
|
||||
$plan[] = lg_noti_step('cart', 'ct_id', $item, array('ct_status'=>$status), array('od_id'));
|
||||
}
|
||||
}
|
||||
// 출고에 사용하는 주문 상태는 장바구니 반영 뒤 마지막에 바꾼다.
|
||||
$plan[] = lg_noti_step('order', 'od_id', $od,
|
||||
array('od_receipt_price'=>$new_receipt, 'od_receipt_time'=>$time, 'od_misu'=>$new_misu,
|
||||
'od_status'=>$status, 'od_casseqno'=>$data['LGD_CASSEQNO']),
|
||||
array('od_pg', 'od_tno', 'od_lg_mid', 'od_settle_case', 'od_test', 'od_bank_account', 'od_cart_price',
|
||||
'od_send_cost', 'od_send_cost2', 'od_cart_coupon', 'od_coupon', 'od_send_coupon', 'od_cancel_price', 'od_receipt_point', 'od_refund_price'));
|
||||
}
|
||||
$json = json_encode($plan);
|
||||
if ($json === false) return false;
|
||||
lg_noti_query("INSERT INTO `{$tables['event']}` SET ln_key='$key', ln_trade='$trade', ln_seq='$seq', ln_flag='$flag', ln_payload='$payload', od_id=".lg_noti_quote($od_id)
|
||||
.", ln_plan=".lg_noti_quote($json).", ln_created_at=".lg_noti_quote($time));
|
||||
if (!lg_noti_apply($plan, $tables)) return false;
|
||||
lg_noti_query("UPDATE `{$tables['event']}` SET ln_done = 1 WHERE ln_key = '$key'");
|
||||
if ($canceling && $od && in_array($od['od_status'], array('배송', '완료'), true)) {
|
||||
error_log('LG notification: bank cancellation after shipment; operator reconciliation required');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ function g5_migration_replace_placeholders($sql)
|
||||
'g5_shop_order_table' => $shop_prefix . 'order',
|
||||
'g5_shop_personalpay_table' => $shop_prefix . 'personalpay',
|
||||
'g5_shop_kcp_noti_table' => $shop_prefix . 'kcp_noti',
|
||||
'g5_shop_lg_noti_table' => $shop_prefix . 'lg_noti',
|
||||
'g5_shop_default_table' => $shop_prefix . 'default',
|
||||
'g5_shop_coupon_table' => $shop_prefix . 'coupon',
|
||||
'g5_shop_coupon_log_table' => $shop_prefix . 'coupon_log',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- @description LG 가상계좌 통보 거래 바인딩 및 재통보 복구 이력 추가
|
||||
-- @if-table-exists {{g5_shop_order_table}}
|
||||
-- @if-column-missing {{g5_shop_order_table}} od_lg_mid
|
||||
ALTER TABLE `{{g5_shop_order_table}}` ADD `od_lg_mid` varchar(104) NOT NULL DEFAULT '';
|
||||
-- @if-table-exists {{g5_shop_personalpay_table}}
|
||||
-- @if-column-missing {{g5_shop_personalpay_table}} pp_lg_mid
|
||||
ALTER TABLE `{{g5_shop_personalpay_table}}` ADD `pp_lg_mid` varchar(104) NOT NULL DEFAULT '';
|
||||
-- @if-table-exists {{g5_shop_default_table}}
|
||||
-- @if-table-missing {{g5_shop_lg_noti_table}}
|
||||
CREATE TABLE `{{g5_shop_lg_noti_table}}` (
|
||||
`ln_key` char(64) NOT NULL,
|
||||
`ln_trade` char(64) NOT NULL,
|
||||
`ln_seq` int(11) NOT NULL,
|
||||
`ln_flag` char(1) NOT NULL,
|
||||
`ln_payload` char(64) NOT NULL,
|
||||
`od_id` bigint(20) unsigned NOT NULL DEFAULT '0',
|
||||
`ln_plan` mediumtext NOT NULL,
|
||||
`ln_done` tinyint(4) NOT NULL DEFAULT '0',
|
||||
`ln_created_at` datetime NOT NULL,
|
||||
PRIMARY KEY (`ln_key`),
|
||||
KEY `ln_trade` (`ln_trade`),
|
||||
KEY `od_id` (`od_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||
@@ -582,6 +582,9 @@ $od_pg = $default['de_pg_service'];
|
||||
// KCP 통보는 결제 당시 사용한 상점코드와 대조한다.
|
||||
$od_kcp_site_cd = ($od_pg === 'kcp' && isset($g_conf_site_cd))
|
||||
? sql_escape_string($g_conf_site_cd) : '';
|
||||
// LG 통보는 결제 당시 서버에서 구성한 운영/테스트 MID와 대조한다.
|
||||
$od_lg_mid = ($od_pg === 'lg' && isset($LGD_MID))
|
||||
? sql_escape_string($LGD_MID) : '';
|
||||
|
||||
// 주문금액과 결제금액이 일치하는지 체크
|
||||
if($tno) {
|
||||
@@ -685,6 +688,7 @@ $sql = " insert {$g5['g5_shop_order_table']}
|
||||
od_misu = '$od_misu',
|
||||
od_pg = '$od_pg',
|
||||
od_kcp_site_cd = '$od_kcp_site_cd',
|
||||
od_lg_mid = '$od_lg_mid',
|
||||
od_tno = '$od_tno',
|
||||
od_app_no = '$od_app_no',
|
||||
od_escrow = '$od_escrow',
|
||||
|
||||
@@ -218,11 +218,15 @@ $pp_pg = $default['de_pg_service'];
|
||||
// KCP 통보는 결제 당시 사용한 상점코드와 대조한다.
|
||||
$pp_kcp_site_cd = ($pp_pg === 'kcp' && isset($g_conf_site_cd))
|
||||
? sql_escape_string($g_conf_site_cd) : '';
|
||||
// LG 통보는 결제 당시 서버에서 구성한 운영/테스트 MID와 대조한다.
|
||||
$pp_lg_mid = ($pp_pg === 'lg' && isset($LGD_MID))
|
||||
? sql_escape_string($LGD_MID) : '';
|
||||
|
||||
// 결제정보 입력
|
||||
$sql = " update {$g5['g5_shop_personalpay_table']}
|
||||
set pp_pg = '$pp_pg',
|
||||
pp_kcp_site_cd = '$pp_kcp_site_cd',
|
||||
pp_lg_mid = '$pp_lg_mid',
|
||||
pp_tno = '$pp_tno',
|
||||
pp_app_no = '$app_no',
|
||||
pp_receipt_price = '$pp_receipt_price',
|
||||
|
||||
@@ -539,6 +539,9 @@ $od_pg = $default['de_pg_service'];
|
||||
// KCP 통보는 결제 당시 사용한 상점코드와 대조한다.
|
||||
$od_kcp_site_cd = ($od_pg === 'kcp' && isset($g_conf_site_cd))
|
||||
? sql_escape_string($g_conf_site_cd) : '';
|
||||
// LG 통보는 결제 당시 서버에서 구성한 운영/테스트 MID와 대조한다.
|
||||
$od_lg_mid = ($od_pg === 'lg' && isset($LGD_MID))
|
||||
? sql_escape_string($LGD_MID) : '';
|
||||
|
||||
$tno = isset($tno) ? $tno : '';
|
||||
$od_receipt_time = isset($od_receipt_time) ? $od_receipt_time : '';
|
||||
@@ -640,6 +643,7 @@ $sql = " insert {$g5['g5_shop_order_table']}
|
||||
od_misu = '$od_misu',
|
||||
od_pg = '$od_pg',
|
||||
od_kcp_site_cd = '$od_kcp_site_cd',
|
||||
od_lg_mid = '$od_lg_mid',
|
||||
od_tno = '$od_tno',
|
||||
od_app_no = '$od_app_no',
|
||||
od_escrow = '$od_escrow',
|
||||
|
||||
@@ -197,6 +197,9 @@ $pp_pg = $default['de_pg_service'];
|
||||
// KCP 통보는 결제 당시 사용한 상점코드와 대조한다.
|
||||
$pp_kcp_site_cd = ($pp_pg === 'kcp' && isset($g_conf_site_cd))
|
||||
? sql_escape_string($g_conf_site_cd) : '';
|
||||
// LG 통보는 결제 당시 서버에서 구성한 운영/테스트 MID와 대조한다.
|
||||
$pp_lg_mid = ($pp_pg === 'lg' && isset($LGD_MID))
|
||||
? sql_escape_string($LGD_MID) : '';
|
||||
$pp_email = get_email_address($pp_email);
|
||||
|
||||
// 결제정보 입력
|
||||
@@ -205,6 +208,7 @@ $sql = " update {$g5['g5_shop_personalpay_table']}
|
||||
pp_hp = '$pp_hp',
|
||||
pp_pg = '$pp_pg',
|
||||
pp_kcp_site_cd = '$pp_kcp_site_cd',
|
||||
pp_lg_mid = '$pp_lg_mid',
|
||||
pp_tno = '$pp_tno',
|
||||
pp_app_no = '$app_no',
|
||||
pp_receipt_price = '$pp_receipt_price',
|
||||
|
||||
+5
-175
@@ -1,178 +1,8 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
include_once(G5_LIB_PATH.'/lg_notification.lib.php');
|
||||
|
||||
|
||||
/*
|
||||
* [상점 결제결과처리(DB) 페이지]
|
||||
*
|
||||
* 1) 위변조 방지를 위한 hashdata값 검증은 반드시 적용하셔야 합니다.
|
||||
*
|
||||
*/
|
||||
$LGD_RESPCODE = isset($_POST["LGD_RESPCODE"]) ? $_POST["LGD_RESPCODE"] : ''; // 응답코드: 0000(성공) 그외 실패
|
||||
$LGD_RESPMSG = isset($_POST["LGD_RESPMSG"]) ? $_POST["LGD_RESPMSG"] : ''; // 응답메세지
|
||||
$LGD_MID = isset($_POST["LGD_MID"]) ? $_POST["LGD_MID"] : ''; // 상점아이디
|
||||
$LGD_OID = isset($_POST["LGD_OID"]) ? $_POST["LGD_OID"] : ''; // 주문번호
|
||||
$LGD_AMOUNT = isset($_POST["LGD_AMOUNT"]) ? $_POST["LGD_AMOUNT"] : ''; // 거래금액
|
||||
$LGD_TID = isset($_POST["LGD_TID"]) ? $_POST["LGD_TID"] : ''; // LG유플러스에서 부여한 거래번호
|
||||
$LGD_PAYTYPE = isset($_POST["LGD_PAYTYPE"]) ? $_POST["LGD_PAYTYPE"] : ''; // 결제수단코드
|
||||
$LGD_PAYDATE = isset($_POST["LGD_PAYDATE"]) ? $_POST["LGD_PAYDATE"] : ''; // 거래일시(승인일시/이체일시)
|
||||
$LGD_HASHDATA = isset($_POST["LGD_HASHDATA"]) ? $_POST["LGD_HASHDATA"] : ''; // 해쉬값
|
||||
$LGD_FINANCECODE = isset($_POST["LGD_FINANCECODE"]) ? $_POST["LGD_FINANCECODE"] : ''; // 결제기관코드(은행코드)
|
||||
$LGD_FINANCENAME = isset($_POST["LGD_FINANCENAME"]) ? $_POST["LGD_FINANCENAME"] : ''; // 결제기관이름(은행이름)
|
||||
$LGD_ESCROWYN = isset($_POST["LGD_ESCROWYN"]) ? $_POST["LGD_ESCROWYN"] : ''; // 에스크로 적용여부
|
||||
$LGD_TIMESTAMP = isset($_POST["LGD_TIMESTAMP"]) ? $_POST["LGD_TIMESTAMP"] : ''; // 타임스탬프
|
||||
$LGD_ACCOUNTNUM = isset($_POST["LGD_ACCOUNTNUM"]) ? $_POST["LGD_ACCOUNTNUM"] : ''; // 계좌번호(무통장입금)
|
||||
$LGD_CASTAMOUNT = isset($_POST["LGD_CASTAMOUNT"]) ? $_POST["LGD_CASTAMOUNT"] : ''; // 입금총액(무통장입금)
|
||||
$LGD_CASCAMOUNT = isset($_POST["LGD_CASCAMOUNT"]) ? $_POST["LGD_CASCAMOUNT"] : ''; // 현입금액(무통장입금)
|
||||
$LGD_CASFLAG = isset($_POST["LGD_CASFLAG"]) ? $_POST["LGD_CASFLAG"] : ''; // 무통장입금 플래그(무통장입금) - 'R':계좌할당, 'I':입금, 'C':입금취소
|
||||
$LGD_CASSEQNO = isset($_POST["LGD_CASSEQNO"]) ? $_POST["LGD_CASSEQNO"] : ''; // 입금순서(무통장입금)
|
||||
$LGD_CASHRECEIPTNUM = isset($_POST["LGD_CASHRECEIPTNUM"]) ? $_POST["LGD_CASHRECEIPTNUM"] : ''; // 현금영수증 승인번호
|
||||
$LGD_CASHRECEIPTSELFYN = isset($_POST["LGD_CASHRECEIPTSELFYN"]) ? $_POST["LGD_CASHRECEIPTSELFYN"] : ''; // 현금영수증자진발급제유무 Y: 자진발급제 적용, 그외 : 미적용
|
||||
$LGD_CASHRECEIPTKIND = isset($_POST["LGD_CASHRECEIPTKIND"]) ? $_POST["LGD_CASHRECEIPTKIND"] : ''; // 현금영수증 종류 0: 소득공제용 , 1: 지출증빙용
|
||||
$LGD_PAYER = isset($_POST["LGD_PAYER"]) ? $_POST["LGD_PAYER"] : ''; // 입금자명
|
||||
|
||||
/*
|
||||
* 구매정보
|
||||
*/
|
||||
$LGD_BUYER = isset($_POST["LGD_BUYER"]) ? $_POST["LGD_BUYER"] : ''; // 구매자
|
||||
$LGD_PRODUCTINFO = isset($_POST["LGD_PRODUCTINFO"]) ? $_POST["LGD_PRODUCTINFO"] : ''; // 상품명
|
||||
$LGD_BUYERID = isset($_POST["LGD_BUYERID"]) ? $_POST["LGD_BUYERID"] : ''; // 구매자 ID
|
||||
$LGD_BUYERADDRESS = isset($_POST["LGD_BUYERADDRESS"]) ? $_POST["LGD_BUYERADDRESS"] : ''; // 구매자 주소
|
||||
$LGD_BUYERPHONE = isset($_POST["LGD_BUYERPHONE"]) ? $_POST["LGD_BUYERPHONE"] : ''; // 구매자 전화번호
|
||||
$LGD_BUYEREMAIL = isset($_POST["LGD_BUYEREMAIL"]) ? $_POST["LGD_BUYEREMAIL"] : ''; // 구매자 이메일
|
||||
$LGD_BUYERSSN = isset($_POST["LGD_BUYERSSN"]) ? $_POST["LGD_BUYERSSN"] : ''; // 구매자 주민번호
|
||||
$LGD_PRODUCTCODE = isset($_POST["LGD_PRODUCTCODE"]) ? $_POST["LGD_PRODUCTCODE"] : ''; // 상품코드
|
||||
$LGD_RECEIVER = isset($_POST["LGD_RECEIVER"]) ? $_POST["LGD_RECEIVER"] : ''; // 수취인
|
||||
$LGD_RECEIVERPHONE = isset($_POST["LGD_RECEIVERPHONE"]) ? $_POST["LGD_RECEIVERPHONE"] : ''; // 수취인 전화번호
|
||||
$LGD_DELIVERYINFO = isset($_POST["LGD_DELIVERYINFO"]) ? $_POST["LGD_DELIVERYINFO"] : ''; // 배송지
|
||||
|
||||
$LGD_MERTKEY = $config['cf_lg_mert_key']; //LG유플러스에서 발급한 상점키로 변경해 주시기 바랍니다.
|
||||
|
||||
$LGD_HASHDATA2 = md5($LGD_MID.$LGD_OID.$LGD_AMOUNT.$LGD_RESPCODE.$LGD_TIMESTAMP.$LGD_MERTKEY);
|
||||
|
||||
/*
|
||||
* 상점 처리결과 리턴메세지
|
||||
*
|
||||
* OK : 상점 처리결과 성공
|
||||
* 그외 : 상점 처리결과 실패
|
||||
*
|
||||
* ※ 주의사항 : 성공시 'OK' 문자이외의 다른문자열이 포함되면 실패처리 되오니 주의하시기 바랍니다.
|
||||
*/
|
||||
$resultMSG = "결제결과 상점 DB처리(LGD_CASNOTEURL) 결과값을 입력해 주시기 바랍니다.";
|
||||
|
||||
|
||||
if ( $LGD_HASHDATA2 == $LGD_HASHDATA ) { //해쉬값 검증이 성공이면
|
||||
if ( "0000" == $LGD_RESPCODE ){ //결제가 성공이면
|
||||
if( "R" == $LGD_CASFLAG ) {
|
||||
/*
|
||||
* 무통장 할당 성공 결과 상점 처리(DB) 부분
|
||||
* 상점 결과 처리가 정상이면 "OK"
|
||||
*/
|
||||
//if( 무통장 할당 성공 상점처리결과 성공 )
|
||||
$resultMSG = "OK";
|
||||
}else if( "I" == $LGD_CASFLAG ) {
|
||||
/*
|
||||
* 무통장 입금 성공 결과 상점 처리(DB) 부분
|
||||
* 상점 결과 처리가 정상이면 "OK"
|
||||
*/
|
||||
|
||||
$sql = " select pp_id, od_id from {$g5['g5_shop_personalpay_table']} where pp_id = '$LGD_OID' and pp_tno = '$LGD_TID' ";
|
||||
$row = sql_fetch($sql);
|
||||
|
||||
$result = false;
|
||||
|
||||
if(isset($row['pp_id']) && $row['pp_id']) {
|
||||
// 개인결제 UPDATE
|
||||
$sql = " update {$g5['g5_shop_personalpay_table']}
|
||||
set pp_receipt_price = '$LGD_AMOUNT',
|
||||
pp_receipt_time = '$LGD_PAYDATE',
|
||||
pp_casseqno = '$LGD_CASSEQNO'
|
||||
where pp_id = '$LGD_OID'
|
||||
and pp_tno = '$LGD_TID' ";
|
||||
$result = sql_query($sql, false);
|
||||
|
||||
if($row['od_id']) {
|
||||
// 주문서 UPDATE
|
||||
$receipt_time = preg_replace("/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})/", "\\1-\\2-\\3 \\4:\\5:\\6", $LGD_PAYDATE);
|
||||
$sql = " update {$g5['g5_shop_order_table']}
|
||||
set od_receipt_price = od_receipt_price + '$LGD_AMOUNT',
|
||||
od_receipt_time = '$LGD_PAYDATE',
|
||||
od_casseqno = '$LGD_CASSEQNO',
|
||||
od_shop_memo = concat(od_shop_memo, \"\\n개인결제 ".$row['pp_id']." 로 결제완료 - ".$receipt_time."\")
|
||||
where od_id = '{$row['od_id']}' ";
|
||||
$result = sql_query($sql, FALSE);
|
||||
}
|
||||
} else {
|
||||
// 주문서 UPDATE
|
||||
$sql = " update {$g5['g5_shop_order_table']}
|
||||
set od_receipt_price = '$LGD_AMOUNT',
|
||||
od_receipt_time = '$LGD_PAYDATE',
|
||||
od_casseqno = '$LGD_CASSEQNO'
|
||||
where od_id = '$LGD_OID'
|
||||
and od_tno = '$LGD_TID' ";
|
||||
$result = sql_query($sql, FALSE);
|
||||
}
|
||||
|
||||
if($result) {
|
||||
if(isset($row['od_id']) && $row['od_id'])
|
||||
$od_id = $row['od_id'];
|
||||
else
|
||||
$od_id = $LGD_OID;
|
||||
|
||||
// 주문정보 체크
|
||||
$sql = " select count(od_id) as cnt
|
||||
from {$g5['g5_shop_order_table']}
|
||||
where od_id = '$od_id'
|
||||
and od_status = '주문' ";
|
||||
$row = sql_fetch($sql);
|
||||
|
||||
if($row['cnt'] == 1) {
|
||||
// 미수금 정보 업데이트
|
||||
$info = get_order_info($od_id);
|
||||
|
||||
$sql = " update {$g5['g5_shop_order_table']}
|
||||
set od_misu = '{$info['od_misu']}' ";
|
||||
if($info['od_misu'] == 0)
|
||||
$sql .= " , od_status = '입금' ";
|
||||
$sql .= " where od_id = '$od_id' ";
|
||||
$result = sql_query($sql, FALSE);
|
||||
|
||||
// 장바구니 상태변경
|
||||
if($info['od_misu'] == 0) {
|
||||
$sql = " update {$g5['g5_shop_cart_table']}
|
||||
set ct_status = '입금'
|
||||
where od_id = '$od_id' ";
|
||||
sql_query($sql, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if( 무통장 입금 성공 상점처리결과 성공 )
|
||||
if ($result)
|
||||
$resultMSG = "OK";
|
||||
else
|
||||
$resultMSG = "DB Error";
|
||||
}else if( "C" == $LGD_CASFLAG ) {
|
||||
/*
|
||||
* 무통장 입금취소 성공 결과 상점 처리(DB) 부분
|
||||
* 상점 결과 처리가 정상이면 "OK"
|
||||
*/
|
||||
//if( 무통장 입금취소 성공 상점처리결과 성공 )
|
||||
$resultMSG = "OK";
|
||||
}
|
||||
} else { //결제가 실패이면
|
||||
/*
|
||||
* 거래실패 결과 상점 처리(DB) 부분
|
||||
* 상점결과 처리가 정상이면 "OK"
|
||||
*/
|
||||
//if( 결제실패 상점처리결과 성공 )
|
||||
$resultMSG = "OK";
|
||||
}
|
||||
} else { //해쉬값이 검증이 실패이면
|
||||
/*
|
||||
* hashdata검증 실패 로그를 처리하시기 바랍니다.
|
||||
*/
|
||||
$resultMSG = "결제결과 상점 DB처리(LGD_CASNOTEURL) 해쉬값 검증이 실패하였습니다.";
|
||||
}
|
||||
|
||||
echo $resultMSG;
|
||||
$data = lg_noti_request($_POST, $_SERVER, $config);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
echo $data !== false && lg_noti_process($data) ? 'OK' : 'FAIL';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
||||
|
||||
define('G5_VERSION', '그누보드5');
|
||||
define('G5_GNUBOARD_VER', '5.6.391');
|
||||
define('G5_GNUBOARD_VER', '5.6.40');
|
||||
// 그누보드5.4.5.5 버전과 영카트5.4.5.5.1 버전을 합쳐서 그누보드5.4.6 버전에서 시작함 (kagla-210617)
|
||||
// G5_YOUNGCART_VER 이 상수를 사용하는 곳이 있으므로 주석 처리 해제함
|
||||
// 그누보드5.4.6 이상 버전 부터는 영카트를 그누보드에 포함하여 배포하므로 영카트5의 버전은 의미가 없습니다.
|
||||
|
||||
Reference in New Issue
Block a user