diff --git a/adm/shop_admin/configform.php b/adm/shop_admin/configform.php index b6c295e8e..53c001164 100644 --- a/adm/shop_admin/configform.php +++ b/adm/shop_admin/configform.php @@ -24,13 +24,13 @@ $pg_anchor = ''; - -if( function_exists('pg_setting_check') ){ - pg_setting_check(true); -} - +
  • SMS설정
  • +'; + +if( function_exists('pg_setting_check') ){ + pg_setting_check(true); +} + ?> @@ -502,7 +502,7 @@ if( function_exists('pg_setting_check') ){ - + >
    - >
    - >
    - > + $easy_provider) { ?> + > +
    + - + >
    @@ -659,7 +659,7 @@ if( function_exists('pg_setting_check') ){ - 개발자센터 -> API키 -> 머트 키에서 확인하실 수 있습니다. 예) 95160cce09854ef44d2edb2bfb05f9f3\n실결제용 [라이브] 키와 테스트용 [테스트] 키는 서로 다르므로, 테스트로 결제시에는 [테스트] 키로 변경하여 사용해주시기 바랍니다."); ?> + 개발자센터 -> API키 -> 머트 키에서 확인하실 수 있습니다. 예) 95160cce09854ef44d2edb2bfb05f9f3\n실결제용 [라이브] 키와 테스트용 [테스트] 키는 서로 다르므로, 테스트로 결제시에는 [테스트] 키로 변경하여 사용해주시기 바랍니다."); ?> @@ -747,34 +747,18 @@ if( function_exists('pg_setting_check') ){ 일 - - - - 삼성페이 서비스신청하기 - + 'KG이니시스', 'toss' => '토스페이먼츠 API') as $easy_pg => $easy_title) { ?> + + 간편결제 - 삼성페이 사용 계약을 하신 경우에만 체크해주세요. INIpay PRO 사용 시 PC와 모바일 주문서에 삼성페이가 노출되며 삼성페이 결제창을 직접 호출합니다. 구버전 결제에서는 모바일 주문서에만 노출됩니다.
    실결제 시 상점 아이디와 사용 중인 결제모듈의 인증키(PRO: HashKey, 구버전: 웹결제 사인키)를 입력해 주세요.", 50); ?> - > - - - - - - - - 실결제 시 상점 아이디와 사용 중인 결제모듈의 인증키(PRO: HashKey, 구버전: 웹결제 사인키)를 입력해 주세요.", 50); ?> - > - - - - - - - - 실결제 시 상점 아이디와 사용 중인 결제모듈의 인증키(PRO: HashKey, 구버전: 웹결제 사인키)를 입력해 주세요.", 50); ?> - > + + $easy_provider) { ?> + > +
    + + @@ -805,14 +789,10 @@ if( function_exists('pg_setting_check') ){ 간편결제는 테스트결제가 되지 않습니다. 실결제에만 정상작동 합니다.\n애플페이는 IOS 기기에 모바일결제만 가능합니다."); ?> - >
    - >
    - >
    - >
    - >
    - >
    - >
    - > + $easy_provider) { ?> + > +
    + @@ -1652,10 +1632,10 @@ function fconfig_check(f) if( f.de_kcp_mid.value && f.de_kcp_site_key.value && parseInt(f.de_card_test.value) > 0 ){ pg_msg = "NHN KCP"; } - } else if ( f.de_pg_service.value == "lg" ) { - if( f.cf_lg_mid.value && f.cf_lg_mert_key.value && parseInt(f.de_card_test.value) > 0 ){ - msg += "(주의!) 토스페이먼츠(구버전) 결제의 결제 설정이 현재 테스트결제로 되어 있습니다.\nMERT KEY를 [테스트]키로 설정한 후 테스트결제를 진행해주세요.\n쇼핑몰 운영중이면 반드시 실결제 전환 및 [라이브]키로 설정하여 운영하셔야 합니다.\n실결제로 변경하려면 결제설정 탭 -> 결제 테스트에서 실결제를 선택해 주세요.\n정말로 테스트결제로 설정하시겠습니까?"; - } + } else if ( f.de_pg_service.value == "lg" ) { + if( f.cf_lg_mid.value && f.cf_lg_mert_key.value && parseInt(f.de_card_test.value) > 0 ){ + msg += "(주의!) 토스페이먼츠(구버전) 결제의 결제 설정이 현재 테스트결제로 되어 있습니다.\nMERT KEY를 [테스트]키로 설정한 후 테스트결제를 진행해주세요.\n쇼핑몰 운영중이면 반드시 실결제 전환 및 [라이브]키로 설정하여 운영하셔야 합니다.\n실결제로 변경하려면 결제설정 탭 -> 결제 테스트에서 실결제를 선택해 주세요.\n정말로 테스트결제로 설정하시겠습니까?"; + } } else if ( f.de_pg_service.value == "toss" ) { if( f.cf_lg_mid.value && f.cf_toss_client_key.value && f.cf_toss_secret_key.value && parseInt(f.de_card_test.value) > 0 ){ msg += "(주의!) 토스페이먼츠 결제의 결제 설정이 현재 테스트결제로 되어 있습니다.\n상점 API키를 [테스트]키로 설정한 후 테스트결제를 진행해주세요.\n쇼핑몰 운영중이면 반드시 실결제 전환 및 [라이브]키로 설정하여 운영하셔야 합니다.\n실결제로 변경하려면 결제설정 탭 -> 결제 테스트에서 실결제를 선택해 주세요.\n정말로 테스트결제로 설정하시겠습니까?"; diff --git a/adm/shop_admin/configformupdate.php b/adm/shop_admin/configformupdate.php index 682675220..3b0b71986 100644 --- a/adm/shop_admin/configformupdate.php +++ b/adm/shop_admin/configformupdate.php @@ -88,15 +88,16 @@ if(isset($_POST['de_taxsave_types_transfer']) && $_POST['de_taxsave_types_transf $de_taxsave_types .= ',transfer'; } -// NHN_KCP 간편결제 체크 -$de_easy_pay_services = ''; -if(isset($_POST['de_easy_pays'])){ - $tmps = array(); - foreach( (array) $_POST['de_easy_pays'] as $v ){ - $tmps[] = preg_replace('/[^0-9a-z_\-]/i', '', $v); - } - $de_easy_pay_services = implode(",", $tmps); +// 알려진 PG별 간편결제 설정만 저장한다. 선택 해제도 명시적으로 보존한다. +$easy_allowed = array('global_nhnkcp_naverpay', 'used_nhnkcp_naverpay_point'); +foreach (array('kcp', 'inicis', 'toss', 'nicepay') as $easy_pg) { + $easy_allowed = array_merge($easy_allowed, array_keys(shop_easypay_catalog($easy_pg))); } +$easy_selected = array(); +foreach (isset($_POST['de_easy_pays']) ? (array) $_POST['de_easy_pays'] : array() as $easy_key) { + if (is_string($easy_key) && in_array($easy_key, $easy_allowed, true)) $easy_selected[] = $easy_key; +} +$de_easy_pay_services = implode(',', array_unique(array_merge($easy_selected, array('inicis_configured', 'toss_configured')))); //KVE-2019-0689, KVE-2019-0691, KVE-2019-0694 $check_sanitize_keys = array( @@ -264,6 +265,11 @@ foreach( $check_sanitize_keys as $key ){ } } +// 구버전 결제 코드 및 기존 스킨과의 호환을 위해 개별 컬럼도 동기화한다. +foreach (shop_easypay_legacy_keys() as $easy_key => $legacy_key) { + $$legacy_key = (int) in_array($easy_key, $easy_selected, true); +} + $de_inicis_pro_use = !empty($de_inicis_pro_use) ? 1 : 0; $de_inicis_hash_key = preg_replace('/[^A-Za-z0-9+\/=_-]/', '', $de_inicis_hash_key); diff --git a/css/default_shop.css b/css/default_shop.css index 8f6191f2e..da9aff643 100644 --- a/css/default_shop.css +++ b/css/default_shop.css @@ -1280,3 +1280,7 @@ html.no-overflowscrolling #sc_coupon_frm, html.no-overflowscrolling #od_coupon_f .chk_box input[type="radio"]:checked + label {color:#305af9} .chk_box input[type="radio"]:checked + label span:before {width:7px;height:7px;background:#305af9;content:'';position:absolute;top:3px;left:3px;border-radius:50%} + +/* PG별 간편결제 공통 버튼 */ +#sod_frm_paysel .samsungpay_icon{background:url('../img/samsungpay.png') no-repeat 50% 50% #fff;overflow:hidden;text-indent:-999px} +#sod_frm_paysel .tosspay_icon,#sod_frm_paysel .pinpay_icon{font-weight:bold} diff --git a/css/mobile_shop.css b/css/mobile_shop.css index 748730fc3..d8aea16c6 100644 --- a/css/mobile_shop.css +++ b/css/mobile_shop.css @@ -1038,3 +1038,7 @@ box-shadow:0 0 8px rgba(65,98,255,0.8)} .chk_box input[type="radio"]:checked + label {color:#305af9} .chk_box input[type="radio"]:checked + label span:before {width:7px;height:7px;background:#305af9;content:'';position:absolute;top:3px;left:3px;border-radius:50%} + +/* PG별 간편결제 공통 버튼 */ +#sod_frm_paysel .samsungpay_icon{background:url('../img/samsungpay.png') no-repeat 50% 50% #fff;overflow:hidden;text-indent:-999px} +#sod_frm_paysel .tosspay_icon,#sod_frm_paysel .pinpay_icon{font-weight:bold} diff --git a/docs/pg-easypay.md b/docs/pg-easypay.md new file mode 100644 index 000000000..679e0b3d8 --- /dev/null +++ b/docs/pg-easypay.md @@ -0,0 +1,43 @@ +# PG별 간편결제 설정과 검증 + +관리자 **쇼핑몰 설정 → 결제설정**에서 **PG사 간편결제 버튼 사용**을 `노출함`으로 설정한 뒤, 선택한 PG와 계약한 수단만 체크한다. 계약 상태는 PG사에 직접 확인해야 하며 관리자 화면에서 자동 조회하지 않는다. + +| PG | 개별 수단 | 제한 | +| --- | --- | --- | +| NHN KCP | PAYCO, 네이버페이, 카카오페이, 애플페이 | 기존 직접 호출과 네이버페이 카드·머니/포인트 분리 유지 | +| KG이니시스 | 삼성페이, L.pay, 카카오페이 | 구 INIpay 삼성페이는 모바일만, PRO는 PC·모바일 지원. 범용 KPAY 버튼 제거 | +| 토스페이먼츠 API | 토스페이, 네이버페이, 삼성페이, 애플페이, L.pay, 카카오페이, 핀페이, PAYCO, SSG페이 | 계약된 수단의 자체창 호출 | +| NICEPAY | 삼성페이, 네이버페이, 카카오페이, 애플페이, PAYCO, SK페이, SSG페이, L.pay | 기존 요청 파라미터 유지. 네이버페이는 `E020=CARD` 카드 경로 | + +애플페이는 모든 PG에서 iOS 모바일에만 표시한다. 삼성페이는 PC에서 휴대폰 연결이 가능하며 구 INIpay만 모바일 전용이다. 표시 조건은 결제 가능 여부를 보증하지 않으며, 실제 기기·브라우저·MID 계약 상태를 확인해야 한다. NICEPAY 간편결제 및 KCP 네이버페이·카카오페이는 기존 관리자 안내처럼 테스트 결제가 제한된다. 다른 수단도 PG에 테스트 지원 여부를 확인한다. + +## KCP 네이버페이 병용 우선순위 + +선택한 PG가 네이버페이를 지원하고 해당 수단을 사용 설정했다면 기본 PG만 제공한다. 기본 PG가 네이버페이를 지원하지 않거나 사용 설정하지 않았다면 병용 설정과 KCP 인증정보를 확인하여 KCP 경로를 제공한다. 중복 버튼뿐 아니라 병용 스크립트·요청 처리에도 동일한 판정을 적용한다. + +기본 PG의 네이버페이가 활성화되어 있으면 KCP 머니/포인트 버튼도 숨긴다. 기본 PG마다 카드·머니 지원 범위가 다르므로, 예를 들어 NICEPAY의 카드 경로 대신 KCP 머니/포인트가 필요하다면 NICEPAY 네이버페이를 해제하고 KCP 병용과 포인트 사용을 설정한다. 기존 주문에 저장된 `od_pg`, 거래번호, 인증정보와 조회·영수증·취소 코드는 유지한다. + +## 설정 저장과 배포 + +`lib/shop.easypay.lib.php`의 목록을 관리자와 PC·모바일 주문서가 함께 사용한다. `de_easy_pay_services`에 PG별 키를 저장한다. 첫 저장 전에는 기존 이니시스 개별 설정과 토스 PAYCO를 승계한다. 저장 후에는 `inicis_configured`, `toss_configured` 표식으로 전체 선택 해제도 보존하며, 기존 이니시스 컬럼을 동기화하여 결제 모듈과의 호환을 유지한다. + +기존 설치는 새 설정을 저장하기 전에 관리자 **환경설정 → DB업그레이드**에서 `20260909_001_pg_easypay_services`를 적용한다. 저장 공간을 `varchar(255)`에서 `varchar(1024)`로 확장하며 신규 설치 SQL도 같은 크기를 사용한다. 일반 페이지 접근으로 스키마를 변경하지 않는다. + +## 검증 + +DB나 외부 PG 연결 없이 실행하는 회귀 테스트: + +```bash +php tests/pg_easypay.php +node tests/pg_easypay.js +``` + +- PG별 버튼과 선택값, 기본 PG 우선과 KCP 병용 전환, 인증정보 누락, 전체 비활성, 기존 설정 승계 및 명시적 선택 해제 +- PC·모바일 구분, iOS·Android 애플페이, 구 INIpay 삼성페이 노출 +- 토스 9개 provider의 실제 SDK 요청 생성 함수에 SDK 대역을 연결하여 직접 호출과 일반 카드 재선택 검증 +- NICEPAY 직접 호출 코드와 일반 카드 재선택 시 직접 호출 옵션·에스크로 복원 +- 토스 승인 성공 처리에 카드 없는 전액 머니 결제와 복합결제 응답을 투입하여 총 승인금액, provider, 승인번호, 현금영수증 저장 검증 + +실제 PG 네트워크 요청과 운영 DB 마이그레이션은 이 테스트에서 실행하지 않는다. 배포 검수 시 계약된 MID에서 각 수단의 성공·실패·사용자 취소·재시도, 주문내역·관리자·영수증 표시, 전액 취소·부분 취소를 확인한다. 머니/포인트 및 카드 복합결제는 실제 총액·현금영수증·부분 취소 가능 여부도 확인한다. 커스텀 스킨이 주문서를 자체 구현했다면 공통 버튼 함수와 결제수단 변경 이벤트를 반영해야 한다. + +토스 자체창 요청은 [SDK v2 결제창 문서](https://docs.tosspayments.com/sdk/v2/js/payment)의 `method=CARD`, `card.flowMode=DIRECT`, `card.easyPay` 규격을 사용한다. provider 코드는 [토스 ENUM 문서](https://docs.tosspayments.com/codes/enum-codes)를 따른다. 일반 카드로 변경하면 `DEFAULT`로 되돌리고 SDK 요청에서 `easyPay`를 제외한다. diff --git a/install/gnuboard5shop.sql b/install/gnuboard5shop.sql index 27526b8ad..680cde423 100644 --- a/install/gnuboard5shop.sql +++ b/install/gnuboard5shop.sql @@ -364,7 +364,7 @@ CREATE TABLE IF NOT EXISTS `g5_shop_default` ( `de_inicis_pro_monitor_message` varchar(255) NOT NULL DEFAULT '', `de_iche_use` tinyint(4) NOT NULL DEFAULT '0', `de_easy_pay_use` tinyint(4) NOT NULL DEFAULT '0', - `de_easy_pay_services` varchar(255) NOT NULL DEFAULT '', + `de_easy_pay_services` varchar(1024) NOT NULL DEFAULT '', `de_samsung_pay_use` tinyint(4) NOT NULL DEFAULT '0', `de_inicis_lpay_use` tinyint(4) NOT NULL DEFAULT '0', `de_inicis_kakaopay_use` tinyint(4) NOT NULL DEFAULT '0', diff --git a/lib/shop.easypay.lib.php b/lib/shop.easypay.lib.php new file mode 100644 index 000000000..ec97ed7dd --- /dev/null +++ b/lib/shop.easypay.lib.php @@ -0,0 +1,123 @@ + 표시명, PG 요청 코드, CSS 클래스, 기존 주문 결제수단 값(선택). +function shop_easypay_catalog($pg) +{ + $catalog = array( + 'kcp' => array( + 'nhnkcp_payco' => array('PAYCO', 'payco', 'PAYCO'), + 'nhnkcp_naverpay' => array('네이버페이', 'naverpay', 'naverpay_icon'), + 'nhnkcp_kakaopay' => array('카카오페이', 'kakaopay', 'kakaopay_icon'), + 'nhnkcp_applepay' => array('애플페이', 'applepay', 'applepay_icon'), + ), + 'inicis' => array( + 'inicis_samsungpay' => array('삼성페이', 'samsungpay', 'samsungpay_icon', '삼성페이'), + 'inicis_lpay' => array('L.pay', 'lpay', 'lpay_icon', 'lpay'), + 'inicis_kakaopay' => array('카카오페이', 'inicis_kakaopay', 'kakaopay_icon', 'inicis_kakaopay'), + ), + 'toss' => array( + 'toss_tosspay' => array('토스페이', 'TOSSPAY', 'tosspay_icon'), + 'toss_naverpay' => array('네이버페이', 'NAVERPAY', 'naverpay_icon'), + 'toss_samsungpay' => array('삼성페이', 'SAMSUNGPAY', 'samsungpay_icon'), + 'toss_applepay' => array('애플페이', 'APPLEPAY', 'applepay_icon'), + 'toss_lpay' => array('L.pay', 'LPAY', 'lpay_icon'), + 'toss_kakaopay' => array('카카오페이', 'KAKAOPAY', 'kakaopay_icon'), + 'toss_pinpay' => array('핀페이', 'PINPAY', 'pinpay_icon'), + 'toss_payco' => array('PAYCO', 'PAYCO', 'PAYCO'), + 'toss_ssgpay' => array('SSG페이', 'SSG', 'ssgpay_icon'), + ), + 'nicepay' => array( + 'nicepay_samsungpay' => array('삼성페이', 'nice_samsungpay', 'samsungpay_icon'), + 'nicepay_naverpay' => array('네이버페이', 'nice_naverpay', 'naverpay_icon'), + 'nicepay_kakaopay' => array('카카오페이', 'nice_kakaopay', 'kakaopay_icon'), + 'nicepay_applepay' => array('애플페이', 'nice_applepay', 'applepay_icon'), + 'nicepay_paycopay' => array('PAYCO', 'nice_paycopay', 'PAYCO'), + 'nicepay_skpay' => array('SK페이', 'nice_skpay', 'skpay_icon'), + 'nicepay_ssgpay' => array('SSG페이', 'nice_ssgpay', 'ssgpay_icon'), + 'nicepay_lpay' => array('L.pay', 'nice_lpay', 'lpay_icon'), + ), + ); + return isset($catalog[$pg]) ? $catalog[$pg] : array(); +} + +function shop_easypay_legacy_keys() +{ + return array('inicis_samsungpay' => 'de_samsung_pay_use', 'inicis_lpay' => 'de_inicis_lpay_use', 'inicis_kakaopay' => 'de_inicis_kakaopay_use'); +} + +// 첫 설정 저장 전에는 기존 개별 설정과 토스 PAYCO 사용 상태를 승계한다. +function shop_easypay_normalize($settings) +{ + $services = !empty($settings['de_easy_pay_services']) ? explode(',', $settings['de_easy_pay_services']) : array(); + if (!in_array('inicis_configured', $services, true)) { + foreach (shop_easypay_legacy_keys() as $key => $legacy) { + if (!empty($settings[$legacy])) { + $services[] = $key; + if ($settings['de_pg_service'] === 'inicis') $settings['de_easy_pay_use'] = 1; + } + } + } + if (!in_array('toss_configured', $services, true) && $settings['de_pg_service'] === 'toss' && !empty($settings['de_easy_pay_use']) && !array_intersect(array_keys(shop_easypay_catalog('toss')), $services)) { + $services[] = 'toss_payco'; + } + $settings['de_easy_pay_services'] = implode(',', array_unique($services)); + foreach (shop_easypay_legacy_keys() as $key => $legacy) { + $settings[$legacy] = (int) (in_array($key, $services, true) && ($settings['de_pg_service'] !== 'inicis' || !empty($settings['de_easy_pay_use']))); + } + return $settings; +} + +function shop_easypay_enabled($key) +{ + global $default; + return !empty($default['de_easy_pay_use']) && in_array($key, explode(',', $default['de_easy_pay_services']), true); +} + +function shop_easypay_available($key, $mobile) +{ + global $default; + if (strpos($key, 'applepay') !== false) { + // KCP/NICEPAY와 동일하게 iOS 모바일 브라우저에만 제공한다. + $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; + return $mobile && preg_match('/iPhone|iPad|iPod/i', $ua); + } + // 삼성페이는 PC에서 휴대폰 인증으로 연결할 수 있다. 구 INIpay만 모바일 전용이다. + return $key !== 'inicis_samsungpay' || $mobile || !empty($default['de_inicis_pro_use']); +} + +function shop_easypay_button($key, $provider, $mobile, $money = false) +{ + $value = isset($provider[3]) ? $provider[3] : '간편결제'; + $legacy_ids = array('inicis_samsungpay' => 'samsungpay', 'inicis_lpay' => 'inicislpay'); + $id = 'od_settle_'.(isset($legacy_ids[$key]) ? $legacy_ids[$key] : $key); + $attrs = isset($provider[3]) ? ' data-case="'.$provider[1].'"' : ''; + if ($money) $attrs .= ' data-money="1"'; + $html = ' '; + return $mobile ? '
  • '.$html.'
  • ' : $html; +} + +function shop_easypay_buttons($mobile = false) +{ + global $default; + $buttons = array(); + $pg = $default['de_pg_service']; + foreach (shop_easypay_catalog($pg) as $key => $provider) { + if (!shop_easypay_enabled($key) || !shop_easypay_available($key, $mobile)) continue; + $buttons[$key] = shop_easypay_button($key, $provider, $mobile); + } + if ($pg === 'lg' && !empty($default['de_easy_pay_use'])) { + $buttons['paynow'] = shop_easypay_button('easy_pay', array('PAYNOW', '', 'PAYNOW'), $mobile); + } + if (is_use_easypay('global_nhnkcp')) { + $buttons['nhnkcp_naverpay'] = shop_easypay_button('nhnkcp_naverpay', array('네이버페이', 'naverpay', 'naverpay_icon'), $mobile); + } + if (isset($buttons['nhnkcp_naverpay'])) { + $services = explode(',', $default['de_easy_pay_services']); + if (in_array('used_nhnkcp_naverpay_point', $services, true)) { + $buttons['nhnkcp_naverpay'] = shop_easypay_button('nhnkcp_naverpay', array('네이버페이 카드결제', 'naverpay', 'naverpay_icon nhnkcp_icon nhnkcp_card'), $mobile); + $buttons['nhnkcp_naverpay_money'] = shop_easypay_button('nhnkcp_naverpay_money', array('네이버페이 머니/포인트', 'naverpay', 'naverpay_icon nhnkcp_icon nhnkcp_money'), $mobile, true); + } + } + return $buttons; +} diff --git a/lib/shop.lib.php b/lib/shop.lib.php index dd5677936..92dbefcb5 100644 --- a/lib/shop.lib.php +++ b/lib/shop.lib.php @@ -2448,6 +2448,12 @@ function is_use_easypay($payname=''){ $de_easy_pay_service_array = (isset($default['de_easy_pay_services']) && $default['de_easy_pay_services']) ? explode(',', $default['de_easy_pay_services']) : array(); + // 기본 PG에서 계약·사용 설정한 네이버페이를 우선한다. + $pg_easypay_catalog = shop_easypay_catalog($default['de_pg_service']); + if ($payname === 'global_nhnkcp' && isset($pg_easypay_catalog[$default['de_pg_service'].'_naverpay']) && shop_easypay_enabled($default['de_pg_service'].'_naverpay')) { + return false; + } + if($payname === 'global_nhnkcp' && $de_easy_pay_service_array && ('kcp' !== $default['de_pg_service'])){ // NHN_KCP 외 타PG 사용시 if( in_array('global_nhnkcp_naverpay', $de_easy_pay_service_array) && ($default['de_card_test'] || (!$default['de_card_test'] && $default['de_kcp_mid'] && $default['de_kcp_site_key']) ) ){ return true; diff --git a/migrations/20260909_001_pg_easypay_services.sql b/migrations/20260909_001_pg_easypay_services.sql new file mode 100644 index 000000000..a135bcabf --- /dev/null +++ b/migrations/20260909_001_pg_easypay_services.sql @@ -0,0 +1,4 @@ +-- @description PG별 간편결제 설정과 병용 옵션 저장 공간 확장 +-- @if-table-exists {{g5_shop_default_table}} +ALTER TABLE `{{g5_shop_default_table}}` + MODIFY `de_easy_pay_services` varchar(1024) NOT NULL DEFAULT ''; diff --git a/mobile/shop/orderform.sub.php b/mobile/shop/orderform.sub.php index af1a45e41..45525509d 100644 --- a/mobile/shop/orderform.sub.php +++ b/mobile/shop/orderform.sub.php @@ -556,7 +556,7 @@ if(function_exists('is_use_easypay') && is_use_easypay('global_nhnkcp')){ // $escrow_title = "에스크로 "; } - if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || is_inicis_simple_pay()) { + if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || is_use_easypay('global_nhnkcp') || is_inicis_simple_pay()) { echo '
    '; - $temp_point = 0; // 회원이면서 포인트사용이면 if ($is_member && $config['cf_use_point']) @@ -762,7 +656,7 @@ if(function_exists('is_use_easypay') && is_use_easypay('global_nhnkcp')){ // echo '
    '; } - if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || is_inicis_simple_pay()) { + if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || is_use_easypay('global_nhnkcp') || is_inicis_simple_pay()) { echo ''; } @@ -1070,7 +964,7 @@ $(function() { $("#show_pay_btn").css("display", "inline"); }); - $("#od_settle_iche,#od_settle_card,#od_settle_vbank,#od_settle_hp,#od_settle_easy_pay,#od_settle_samsungpay,#od_settle_nhnkcp_payco,#od_settle_nhnkcp_naverpay,#od_settle_nhnkcp_naverpay_money,#od_settle_nhnkcp_kakaopay,#od_settle_inicislpay,#od_settle_inicis_kakaopay").bind("click", function() { + $("input[name=od_settle_case]:not(#od_settle_bank):not(#od_settle_point)").on("click", function() { $("#settle_bank").hide(); $("#show_req_btn").css("display", "inline"); $("#show_pay_btn").css("display", "none"); @@ -1442,8 +1336,17 @@ function pay_approval() f.escrowProducts.value = JSON.stringify(); + f.cardflowMode.value = 'DEFAULT'; + f.cardeasyPay.value = ''; if(settle_method == "간편결제") { + var provider = $("input[name=od_settle_case]:checked").attr("data-pay"); + var providers = ; + if (providers.indexOf(provider) === -1) { + alert('간편결제 수단을 다시 선택해 주세요.'); + return false; + } f.cardflowMode.value = 'DIRECT'; + f.cardeasyPay.value = provider; } f.amountCurrency.value = 'KRW'; @@ -1468,6 +1371,7 @@ function pay_approval() var features = position + ", width=320, height=440"; var p_reserved = f.DEF_RESERVED.value; f.P_RESERVED.value = p_reserved; + f.P_SKIP_TERMS.value = ""; switch(settle_method) { case "계좌이체": paymethod = "bank"; diff --git a/mobile/shop/toss/orderform.1.php b/mobile/shop/toss/orderform.1.php index 7c7207b44..e5f2819ff 100644 --- a/mobile/shop/toss/orderform.1.php +++ b/mobile/shop/toss/orderform.1.php @@ -12,7 +12,7 @@ if (!defined("_GNUBOARD_")) exit; // 개별 페이지 접근 불가 - + diff --git a/mobile/shop/toss/orderform.2.php b/mobile/shop/toss/orderform.2.php index 0d67ed25f..916e31eac 100644 --- a/mobile/shop/toss/orderform.2.php +++ b/mobile/shop/toss/orderform.2.php @@ -11,7 +11,7 @@ if (!defined("_GNUBOARD_")) exit; // 개별 페이지 접근 불가 - + diff --git a/mobile/shop/toss/toss_approval.php b/mobile/shop/toss/toss_approval.php index 0173471a9..398d113d9 100644 --- a/mobile/shop/toss/toss_approval.php +++ b/mobile/shop/toss/toss_approval.php @@ -145,12 +145,14 @@ async function launchCrossPlatform(frm) { // 신용카드 paymentOptions.card = { flowMode: frm.cardflowMode.value, // 통합결제창 여는 옵션 - easyPay: frm.cardeasyPay.value, useCardPoint: frm.cardUseCardPoint.value == "true" ? true : false, useAppCardOnly: frm.cardUseAppCardOnly.value == "true" ? true : false, useEscrow: frm.cardUseEscrow.value == "true" ? true : false, }; + if (frm.cardflowMode.value === 'DIRECT' && frm.cardeasyPay.value) { + paymentOptions.card.easyPay = frm.cardeasyPay.value; + } // escrowProducts 추가 addEscrowProducts(paymentOptions.card); } else if (frm.method.value == 'VIRTUAL_ACCOUNT') { diff --git a/shop.config.php b/shop.config.php index 879dfbfda..c519885b3 100644 --- a/shop.config.php +++ b/shop.config.php @@ -37,6 +37,8 @@ if (G5_HTTPS_DOMAIN) { // 쇼핑몰 설정값 배열변수 $default = sql_fetch(" select * from {$g5['g5_shop_default_table']} "); +include_once(G5_LIB_PATH.'/shop.easypay.lib.php'); +$default = shop_easypay_normalize($default); if(!defined('_THEME_PREVIEW_')) { // 테마 경로 설정 diff --git a/shop/orderform.sub.php b/shop/orderform.sub.php index 8b82bda5d..9d7c90901 100644 --- a/shop/orderform.sub.php +++ b/shop/orderform.sub.php @@ -575,7 +575,7 @@ if(function_exists('is_use_easypay') && is_use_easypay('global_nhnkcp')){ // $escrow_title = "에스크로
    "; } - if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || (empty($inicis_pro_use) && ($default['de_inicis_lpay_use'] || $default['de_inicis_kakaopay_use'])) || (!empty($inicis_pro_use) && is_inicis_simple_pay())) { + if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || is_use_easypay('global_nhnkcp') || (empty($inicis_pro_use) && ($default['de_inicis_lpay_use'] || $default['de_inicis_kakaopay_use'])) || (!empty($inicis_pro_use) && is_inicis_simple_pay())) { echo '
    '; echo '결제방법 선택'; } @@ -617,111 +617,12 @@ if(function_exists('is_use_easypay') && is_use_easypay('global_nhnkcp')){ // $checked = ''; } - $easypay_prints = array(); - - // PG 간편결제 - if($default['de_easy_pay_use']) { - switch($default['de_pg_service']) { - case 'lg': - $pg_easy_pay_name = 'PAYNOW'; - break; - case 'inicis': - $pg_easy_pay_name = 'KPAY'; - break; - default: - $pg_easy_pay_name = 'PAYCO'; - break; - } - - $multi_settle++; - - if(in_array($default['de_pg_service'], array('kcp', 'nicepay')) && isset($default['de_easy_pay_services']) && $default['de_easy_pay_services']) { - $de_easy_pay_service_array = explode(',', $default['de_easy_pay_services']); - - if ($default['de_pg_service'] === 'kcp') { - if( in_array('nhnkcp_payco', $de_easy_pay_service_array) ){ - $easypay_prints['nhnkcp_payco'] = ' '; - } - if( in_array('nhnkcp_naverpay', $de_easy_pay_service_array) ){ - - if(isset($default['de_easy_pay_services']) && in_array('used_nhnkcp_naverpay_point', explode(',', $default['de_easy_pay_services'])) ){ - $easypay_prints['nhnkcp_naverpay_card'] = ' '; - - $easypay_prints['nhnkcp_naverpay_money'] = ' '; - } else { - $easypay_prints['nhnkcp_naverpay_card'] = ' '; - } - - } - if( in_array('nhnkcp_kakaopay', $de_easy_pay_service_array) ){ - $easypay_prints['nhnkcp_kakaopay'] = ' '; - } - } else if ($default['de_pg_service'] === 'nicepay') { - if( in_array('nicepay_samsungpay', $de_easy_pay_service_array) ){ - $easypay_prints['nicepay_samsungpay'] = ' '; - } - if( in_array('nicepay_naverpay', $de_easy_pay_service_array) ){ - $easypay_prints['nicepay_naverpay'] = ' '; - } - if( in_array('nicepay_kakaopay', $de_easy_pay_service_array) ){ - $easypay_prints['nicepay_kakaopay'] = ' '; - } - if( in_array('nicepay_paycopay', $de_easy_pay_service_array) ){ - $easypay_prints['nicepay_paycopay'] = ' '; - } - if( in_array('nicepay_skpay', $de_easy_pay_service_array) ){ - $easypay_prints['nicepay_skpay'] = ' '; - } - if( in_array('nicepay_ssgpay', $de_easy_pay_service_array) ){ - $easypay_prints['nicepay_ssgpay'] = ' '; - } - if( in_array('nicepay_lpay', $de_easy_pay_service_array) ){ - $easypay_prints['nicepay_lpay'] = ' '; - } - } - } else { - $easypay_prints[strtolower($pg_easy_pay_name)] = ' '; - } - - } - - if( ! isset($easypay_prints['nhnkcp_naverpay']) && function_exists('is_use_easypay') && is_use_easypay('global_nhnkcp') ){ - - if(isset($default['de_easy_pay_services']) && in_array('used_nhnkcp_naverpay_point', explode(',', $default['de_easy_pay_services'])) ){ - $easypay_prints['nhnkcp_naverpay_card'] = ' '; - - $easypay_prints['nhnkcp_naverpay_money'] = ' '; - } else { - $easypay_prints['nhnkcp_naverpay'] = ' '; - } - } - - if($easypay_prints) { - $multi_settle++; + $easypay_prints = shop_easypay_buttons(false); + if ($easypay_prints) { + $multi_settle += count($easypay_prints); echo run_replace('shop_orderform_easypay_buttons', implode(PHP_EOL, $easypay_prints), $easypay_prints, $multi_settle); } - //이니시스 PRO 삼성페이 - if(!empty($inicis_pro_use) && $default['de_samsung_pay_use']) { - $multi_settle++; - echo ' '.PHP_EOL; - $checked = ''; - } - - //이니시스 Lpay - if($default['de_inicis_lpay_use']) { - $multi_settle++; - echo ' '.PHP_EOL; - $checked = ''; - } - - //이니시스 카카오페이 - if(isset($default['de_inicis_kakaopay_use']) && $default['de_inicis_kakaopay_use']) { - $multi_settle++; - echo ' '.PHP_EOL; - $checked = ''; - } - $temp_point = 0; // 회원이면서 포인트사용이면 if ($is_member && $config['cf_use_point']) @@ -785,7 +686,7 @@ if(function_exists('is_use_easypay') && is_use_easypay('global_nhnkcp')){ // echo ''; } - if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || (empty($inicis_pro_use) && ($default['de_inicis_lpay_use'] || $default['de_inicis_kakaopay_use'])) || (!empty($inicis_pro_use) && is_inicis_simple_pay())) { + if ($default['de_bank_use'] || $default['de_vbank_use'] || $default['de_iche_use'] || $default['de_card_use'] || $default['de_hp_use'] || $default['de_easy_pay_use'] || is_use_easypay('global_nhnkcp') || (empty($inicis_pro_use) && ($default['de_inicis_lpay_use'] || $default['de_inicis_kakaopay_use'])) || (!empty($inicis_pro_use) && is_inicis_simple_pay())) { echo '
    '; } @@ -1074,7 +975,7 @@ $(function() { $("#settle_bank").show(); }); - $("#od_settle_iche,#od_settle_card,#od_settle_vbank,#od_settle_hp,#od_settle_easy_pay,#od_settle_nhnkcp_payco,#od_settle_nhnkcp_naverpay,#od_settle_nhnkcp_naverpay_money,#od_settle_nhnkcp_kakaopay,#od_settle_inicislpay,#od_settle_inicis_kakaopay").bind("click", function() { + $("input[name=od_settle_case]:not(#od_settle_bank):not(#od_settle_point)").on("click", function() { $("#settle_bank").hide(); }); @@ -1565,6 +1466,7 @@ function forderform_check(f) break; } + f.acceptmethod.value = f.acceptmethod.value.replace(/:cardonly/g, ''); switch(settle_method) { case "계좌이체": @@ -1723,8 +1625,17 @@ function forderform_check(f) f.escrowProducts.value = JSON.stringify(); + f.cardflowMode.value = 'DEFAULT'; + f.cardeasyPay.value = ''; if(settle_method == "간편결제") { + var provider = $("input[name=od_settle_case]:checked").attr("data-pay"); + var providers = ; + if (providers.indexOf(provider) === -1) { + alert('간편결제 수단을 다시 선택해 주세요.'); + return false; + } f.cardflowMode.value = 'DIRECT'; + f.cardeasyPay.value = provider; } f.amountCurrency.value = 'KRW'; diff --git a/shop/toss/orderform.1.php b/shop/toss/orderform.1.php index 509be6904..e33cc31dd 100644 --- a/shop/toss/orderform.1.php +++ b/shop/toss/orderform.1.php @@ -66,12 +66,14 @@ async function launchCrossPlatform(frm) { // 신용카드 paymentOptions.card = { flowMode: frm.cardflowMode.value, // 통합결제창 여는 옵션 - easyPay: frm.cardeasyPay.value, useCardPoint: frm.cardUseCardPoint.value == "true" ? true : false, useAppCardOnly: frm.cardUseAppCardOnly.value == "true" ? true : false, useEscrow: frm.cardUseEscrow.value == "true" ? true : false, }; + if (frm.cardflowMode.value === 'DIRECT' && frm.cardeasyPay.value) { + paymentOptions.card.easyPay = frm.cardeasyPay.value; + } // escrowProducts 추가 addEscrowProducts(paymentOptions.card); } else if (frm.method.value == 'VIRTUAL_ACCOUNT') { diff --git a/shop/toss/orderform.2.php b/shop/toss/orderform.2.php index 79a5ca92c..8cdb27bd5 100644 --- a/shop/toss/orderform.2.php +++ b/shop/toss/orderform.2.php @@ -11,7 +11,7 @@ if (!defined("_GNUBOARD_")) exit; // 개별 페이지 접근 불가 - + diff --git a/shop/toss/toss_result.php b/shop/toss/toss_result.php index dff6257d3..26e191b62 100644 --- a/shop/toss/toss_result.php +++ b/shop/toss/toss_result.php @@ -56,7 +56,7 @@ if ($result) { if ($method === '카드') { // 카드 $app_no = $od_app_no = isset($toss->responseData['card']['approveNo']) ? $toss->responseData['card']['approveNo'] : '00000000'; - $card_name = isset($toss->cardCode[$toss->responseData['card']['issuerCode']]) ? $toss->cardCode[$toss->responseData['card']['issuerCode']] : ''; + $card_name = isset($toss->cardCode[$toss->responseData['card']['issuerCode']]) ? $toss->cardCode[$toss->responseData['card']['issuerCode']] : ''; } else if ($method === '가상계좌') { // 가상계좌 $bank_name = $bankname = isset($toss->bankCode[$toss->responseData['virtualAccount']['bankCode']]) ? $toss->bankCode[$toss->responseData['virtualAccount']['bankCode']] : ''; @@ -66,6 +66,16 @@ if ($result) { // 계좌이체 $bank_name = isset($toss->bankCode[$toss->responseData['transfer']['bankCode']]) ? $toss->bankCode[$toss->responseData['transfer']['bankCode']] : ''; + } else if ($method === '휴대폰') { + // 휴대폰 + $mobile_no = isset($toss->responseData['mobilePhone']['customerMobilePhone']) ? $toss->responseData['mobilePhone']['customerMobilePhone'] : ''; + } else if ($method === '간편결제') { + // 카드가 없는 전액 머니·포인트 결제도 승인번호 없이 정상 처리한다. + $app_no = $od_app_no = isset($toss->responseData['card']['approveNo']) ? $toss->responseData['card']['approveNo'] : ''; + $provider = isset($toss->responseData['easyPay']['provider']) ? $toss->responseData['easyPay']['provider'] : ''; + $card_name = isset($toss->easyPayCode[$provider]) ? $toss->easyPayCode[$provider] : $provider; + } + if (!empty($toss->responseData['cashReceipt'])) { // 현금영수증 데이터 처리 $cashReceiptType = isset($toss->responseData['cashReceipt']['type']) ? $toss->responseData['cashReceipt']['type'] : ''; $RcptType = $cashReceiptType === '소득공제' ? '1' : ($cashReceiptType === '지출증빙' ? '2' : '0'); @@ -79,14 +89,8 @@ if ($result) { $pg_receipt_infos['od_cash_no'] = $RcptAuthCode; // 현금영수증 승인번호 $pg_receipt_infos['od_cash_info'] = serialize(array('TID'=>$RcptTID, 'ApplNum'=>$RcptAuthCode, 'receiptUrl'=>$RcptReceiptUrl)); } - } else if ($method === '휴대폰') { - // 휴대폰 - $mobile_no = isset($toss->responseData['mobilePhone']['customerMobilePhone']) ? $toss->responseData['mobilePhone']['customerMobilePhone'] : ''; - } else if ($method === '간편결제') { - // 간편결제 - $provider = isset($toss->responseData['easyPay']['provider']) ? $toss->responseData['easyPay']['provider'] : ''; - $card_name = isset($toss->easyPayCode[$provider]) ? $toss->easyPayCode[$provider] : $provider; } + } else { if(G5_IS_MOBILE) { diff --git a/tests/pg_easypay.js b/tests/pg_easypay.js new file mode 100644 index 000000000..b471d35a0 --- /dev/null +++ b/tests/pg_easypay.js @@ -0,0 +1,87 @@ +// 실행: node tests/pg_easypay.js (실제 PG SDK 대신 요청을 기록한다) +const fs = require('node:fs'); +const vm = require('node:vm'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const root = path.dirname(__dirname); +const providers = ['TOSSPAY', 'NAVERPAY', 'SAMSUNGPAY', 'APPLEPAY', 'LPAY', 'KAKAOPAY', 'PINPAY', 'PAYCO', 'SSG']; +function read(file) { return fs.readFileSync(path.join(root, file), 'utf8'); } +async function main() { + for (const [orderFile, sdkFile] of [ + ['shop/orderform.sub.php', 'shop/toss/orderform.1.php'], + ['mobile/shop/orderform.sub.php', 'mobile/shop/toss/toss_approval.php'], + ]) { + const source = read(orderFile); + const start = source.indexOf(" f.cardflowMode.value = 'DEFAULT';"); + const end = source.indexOf(" f.amountCurrency.value", start); + const selectionCode = source.slice(start, end).replace(/<\?php[\s\S]*?\?>/g, JSON.stringify(providers)); + let sdk = read(sdkFile); + sdk = sdk.slice(sdk.indexOf('async function launchCrossPlatform'), sdk.indexOf('await payment.requestPayment')) + 'await payment.requestPayment(paymentOptions);\n}'; + sdk = sdk.replace(/<\?php[\s\S]*?\?>/g, 'https://example.test'); + const requests = []; + let selected = ''; + const context = vm.createContext({ + payment: { requestPayment: async options => requests.push(options) }, + $: () => ({ attr: () => selected }), + alert: message => { throw new Error(message); }, + }); + vm.runInContext(sdk + '\nfunction select(f, settle_method) {\n' + selectionCode + '\n}', context); + const form = {}; + for (const key of ['amountValue', 'method', 'taxFreeAmount', 'orderId', 'orderName', 'customerEmail', 'customerName', 'customerMobilePhone', 'cardflowMode', 'cardeasyPay', 'cardUseCardPoint', 'cardUseAppCardOnly', 'cardUseEscrow', 'escrowProducts']) { + form[key] = { value: '' }; + } + form.amountValue.value = '10000'; + form.taxFreeAmount.value = '2000'; + form.method.value = 'CARD'; + form.cardUseEscrow.value = 'false'; + for (const provider of providers) { + selected = provider; + context.select(form, '간편결제'); + await context.launchCrossPlatform(form); + const direct = requests.at(-1); + assert.equal(direct.card.easyPay, provider); + assert.equal(direct.card.flowMode, 'DIRECT'); + assert.equal(direct.amount.value, 10000); + assert.equal(direct.taxFreeAmount, 2000); + context.select(form, '신용카드'); + await context.launchCrossPlatform(form); + const card = requests.at(-1); + assert.equal(card.card.flowMode, 'DEFAULT'); + assert.equal('easyPay' in card.card, false); + } + selected = 'invalid'; + assert.throws(() => context.select(form, '간편결제'), /다시 선택/); + // NICEPAY 선택 후 카드 재선택 시 직접 호출/에스크로 상태도 복원된다. + const niceStart = source.indexOf(' f.DirectShowOpt.value = "";'); + let niceCode = source.slice(niceStart).replace(/<\?php[\s\S]*?\?>/g, ''); + const niceContext = vm.createContext({ jQuery: () => ({ attr: () => selected }) }); + // 모바일은 이 블록에 구매자 정보 복사 등이 이어지므로 switch 끝까지만 실행한다. + const switchStart = niceCode.indexOf('switch(settle_method)'); + const braceStart = niceCode.indexOf('{', switchStart); + let depth = 1, braceEnd = braceStart + 1; + for (; depth && braceEnd < niceCode.length; braceEnd++) { + if (niceCode[braceEnd] === '{') depth++; + if (niceCode[braceEnd] === '}') depth--; + } + niceCode = niceCode.slice(0, braceEnd); + vm.runInContext('function select(f, settle_method) {\n' + niceCode + '\n}', niceContext); + const niceForm = {}; + for (const key of ['DirectShowOpt', 'DirectEasyPay', 'NicepayReserved', 'EasyPayMethod', 'TransType', 'PayMethod']) niceForm[key] = { value: '' }; + for (const [provider, field, code] of [ + ['nice_naverpay', 'DirectEasyPay', 'E020'], ['nice_samsungpay', 'DirectEasyPay', 'E021'], + ['nice_kakaopay', 'NicepayReserved', 'DirectKakao=Y'], ['nice_paycopay', 'NicepayReserved', 'DirectPayco=Y'], + ['nice_skpay', 'NicepayReserved', 'DirectPay11=Y'], ['nice_ssgpay', 'DirectEasyPay', 'E007'], ['nice_lpay', 'DirectEasyPay', 'E018'], + ...(orderFile.startsWith('mobile') ? [['nice_applepay', 'DirectEasyPay', 'E022']] : []), + ]) { + selected = provider; + niceContext.select(niceForm, '간편결제'); + assert.equal(niceForm[field].value, code); + assert.equal(niceForm.TransType.value, '0'); + niceContext.select(niceForm, '신용카드'); + for (const key of ['DirectShowOpt', 'DirectEasyPay', 'NicepayReserved', 'EasyPayMethod']) assert.equal(niceForm[key].value, ''); + assert.equal(niceForm.TransType.value, '1'); + } + } + console.log('PC·모바일 PG 요청/결제수단 전환 테스트 통과'); +} +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/tests/pg_easypay.php b/tests/pg_easypay.php new file mode 100644 index 000000000..1aa1bdc07 --- /dev/null +++ b/tests/pg_easypay.php @@ -0,0 +1,82 @@ + $pg, 'de_easy_pay_services' => $services, + 'de_easy_pay_use' => $use, 'de_card_test' => 1, 'de_kcp_mid' => '', 'de_kcp_site_key' => '', 'de_inicis_pro_use' => 1)); +} +$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)'; +foreach (array('toss', 'nicepay') as $pg) { + $default = settings($pg, $pg.'_naverpay,global_nhnkcp_naverpay'); + foreach (array(false, true) as $mobile) { + $buttons = shop_easypay_buttons($mobile); + check(isset($buttons[$pg.'_naverpay']) && !isset($buttons['nhnkcp_naverpay']), '기본 PG 네이버페이 우선'); + check(!is_use_easypay('global_nhnkcp'), '병용 요청 경로 차단'); + } + $default = settings($pg, 'toss_configured,global_nhnkcp_naverpay'); + check(isset(shop_easypay_buttons()['nhnkcp_naverpay']), '기본 PG 네이버페이 비활성 시 병용'); + $default['de_card_test'] = 0; + check(!is_use_easypay('global_nhnkcp'), '실결제 인증정보 필수'); + $default['de_kcp_mid'] = 'merchant'; $default['de_kcp_site_key'] = 'key'; + check(is_use_easypay('global_nhnkcp'), '병용 인증정보 설정'); +} +foreach (array('kcp', 'inicis', 'toss', 'nicepay') as $pg) { + $default = settings($pg, implode(',', array_keys(shop_easypay_catalog($pg)))); + foreach (array(false, true) as $mobile) { + $buttons = shop_easypay_buttons($mobile); + foreach ($buttons as $key => $html) { + check(isset(shop_easypay_catalog($pg)[$key]), '다른 PG 설정 혼입 금지'); + check((strpos($html, '
  • ') === 0) === $mobile, 'PC/모바일 마크업'); + check(strpos($html, 'data-pay="'.shop_easypay_catalog($pg)[$key][1].'"') !== false, '요청 provider'); + } + check(strpos(implode('', $buttons), 'KPAY') === false, '범용 KPAY 중복 제거'); + } + $default['de_easy_pay_use'] = 0; + check(!shop_easypay_buttons(), '간편결제 전체 비활성'); +} +$default = settings('kcp', 'global_nhnkcp_naverpay'); +check(!shop_easypay_buttons(), '병용 설정이 KCP 기본 네이버페이로 오인되지 않음'); +$default = settings('lg', 'global_nhnkcp_naverpay,used_nhnkcp_naverpay_point', 0); +check(count(shop_easypay_buttons(true)) === 2, '병용 카드/머니 독립 노출'); +$default = settings('inicis', 'inicis_samsungpay'); +$default['de_inicis_pro_use'] = 0; +check(!shop_easypay_buttons(false) && count(shop_easypay_buttons(true)) === 1, '구 INIpay 삼성페이 모바일 제한'); +$default = settings('toss', 'toss_configured,toss_applepay'); +check(!shop_easypay_buttons(false) && count(shop_easypay_buttons(true)) === 1, '애플페이 iOS 모바일'); +$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Linux; Android 14)'; +check(!shop_easypay_buttons(true), 'Android 애플페이 숨김'); +$old = array('de_pg_service' => 'inicis', 'de_easy_pay_use' => 0, 'de_inicis_lpay_use' => 1); +$new = shop_easypay_normalize($old); +check($new['de_easy_pay_use'] && strpos($new['de_easy_pay_services'], 'inicis_lpay') !== false, '기존 이니시스 설정 승계'); +$old['de_easy_pay_services'] = 'inicis_configured'; +check(!shop_easypay_normalize($old)['de_inicis_lpay_use'], '명시적 선택 해제 유지'); +$default = settings('toss', ''); +check(isset(shop_easypay_buttons()['toss_payco']), '기존 토스 PAYCO 승계'); +$default = settings('toss', 'toss_configured'); +check(!shop_easypay_buttons(), '토스 전체 선택 해제 유지'); + +// 실제 승인 성공 처리 블록에 카드 없는 머니 결제와 복합결제 응답을 투입한다. +$source = file_get_contents(dirname(__DIR__).'/shop/toss/toss_result.php'); +$start = strpos($source, ' // 공통 DB처리 변수 설정'); +$end = strpos($source, "\n } else {", $start); +$code = substr($source, $start, $end - $start); +foreach (array(false, true) as $with_card) { + $method = '간편결제'; + $toss = (object) array('easyPayCode' => array('NAVERPAY' => '네이버페이'), 'responseData' => array( + 'paymentKey' => 'fixture-key', 'totalAmount' => 10000, 'useEscrow' => false, 'approvedAt' => '2026-09-09T12:00:00+09:00', + 'easyPay' => array('provider' => 'NAVERPAY', 'amount' => 4000, 'discountAmount' => 1000), + 'cashReceipt' => array('type' => '소득공제', 'receiptKey' => 'receipt', 'issueNumber' => '123', 'receiptUrl' => 'https://example.test/receipt'))); + if ($with_card) $toss->responseData['card'] = array('approveNo' => '456', 'amount' => 5000); + $pg_receipt_infos = array(); + eval($code); + check($amount === 10000 && $card_name === '네이버페이', '총 승인금액과 provider 유지'); + check($app_no === ($with_card ? '456' : ''), '전액 머니/복합결제 승인번호'); + check($pg_receipt_infos['od_cash'] === 1 && $pg_receipt_infos['od_cash_no'] === '123', '간편결제 현금영수증 보존'); +} +echo "PG 간편결제 회귀 테스트 통과\n"; diff --git a/theme/basic/css/default_shop.css b/theme/basic/css/default_shop.css index 76ddaf037..524295842 100644 --- a/theme/basic/css/default_shop.css +++ b/theme/basic/css/default_shop.css @@ -1297,3 +1297,7 @@ html.no-overflowscrolling #sc_coupon_frm, html.no-overflowscrolling #od_coupon_f .chk_box input[type="radio"]:checked + label {color:#305af9} .chk_box input[type="radio"]:checked + label span:before {width:7px;height:7px;background:#305af9;content:'';position:absolute;top:3px;left:3px;border-radius:50%} + +/* PG별 간편결제 공통 버튼 */ +#sod_frm_paysel .samsungpay_icon{background:url('../../../img/samsungpay.png') no-repeat 50% 50% #fff;overflow:hidden;text-indent:-999px} +#sod_frm_paysel .tosspay_icon,#sod_frm_paysel .pinpay_icon{font-weight:bold} diff --git a/theme/basic/css/mobile_shop.css b/theme/basic/css/mobile_shop.css index 471d8853b..4183cbb25 100644 --- a/theme/basic/css/mobile_shop.css +++ b/theme/basic/css/mobile_shop.css @@ -1037,3 +1037,7 @@ box-shadow:0 0 8px rgba(65,98,255,0.8)} .chk_box input[type="radio"]:checked + label {color:#305af9} .chk_box input[type="radio"]:checked + label span:before {width:7px;height:7px;background:#305af9;content:'';position:absolute;top:3px;left:3px;border-radius:50%} + +/* PG별 간편결제 공통 버튼 */ +#sod_frm_paysel .samsungpay_icon{background:url('../../../img/samsungpay.png') no-repeat 50% 50% #fff;overflow:hidden;text-indent:-999px} +#sod_frm_paysel .tosspay_icon,#sod_frm_paysel .pinpay_icon{font-weight:bold}