fix(payment): NHN KCP 간편결제 표시 라벨 보강

This commit is contained in:
thisgun committed 2026-07-03 19:54:51 +09:00
1 parent 2a9f521487
commit 0e9bb39980
22 files changed
+818 -67

No files matched your search

@@ -8,6 +8,7 @@
### Fixed
- 간편결제 주문의 원 결제수단이 사용자 주문완료/주문상세, 관리자 주문목록/주문상세에서 신용카드로만 보이지 않도록 표시 라벨과 관리자 목록 표시 맵을 보강했습니다.
- 결제창 닫힘, 인증 취소, 승인 실패, 금액 불일치 등 NHN KCP 실패 경로에서 주문 실패 상태와 결제 레코드의 PG별 실패 메타가 함께 기록되도록 보강했습니다.
- 결제 실패/취소 후 같은 주문번호로 결제창을 다시 열 때 안전한 실패 주문만 `pending_order` / `ready` 상태로 복구하는 재시도 준비 API를 추가했습니다.
- PC/모바일 결제창 호출 직전에 재시도 준비 API를 호출해 사용자가 결제 실패 후 동일 주문 결제를 다시 시도할 수 있도록 보강했습니다.
+2 -2
View File
@@ -1,4 +1,4 @@
(function(){"use strict";function Xe(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function Ze(e){return e.trim().slice(0,160)}function et(e){if(e)return e.replace(/\/payment\/close-report$/,"/payment/retry")}async function ie(e,t,n=!1){const r=window.G7Core?.api;if(typeof r?.post=="function"){await r.post(e,t);return}const a=await fetch(Xe(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),keepalive:n});if(!a.ok){let o=`NHN KCP payment request failed (${a.status})`;try{const s=await a.json(),i=Array.isArray(s?.errors?.message)?s.errors.message[0]:s?.error??s?.message;typeof i=="string"&&i.trim()!==""&&(o=i)}catch{}throw new Error(o)}}async function tt(e){const t=et(e.closeReportUrl);t&&await ie(t,{oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??""})}async function nt(e,t="kcp-window-closed"){if(!e.closeReportUrl)return;const n={oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??"",reason:Ze(t)||"kcp-window-closed"};try{await ie(e.closeReportUrl,n,!0)}catch(r){console.warn("[sirsoft-pay_nhnkcp] failed to report payment window close",r)}}const rt="nhnkcp_applepay";function I(e){return e===rt}function K(){if(typeof navigator>"u")return!1;const e=navigator,t=(e.userAgent||"").toLowerCase(),n=((e.userAgentData?.platform??e.platform)||"").toLowerCase(),r=e.maxTouchPoints??0;return/iphone|ipad|ipod/.test(t)||/iphone|ipad|ipod|ios/.test(n)?!0:/macintosh|mac os x/.test(t)&&r>1}function $(){return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?"애플페이는 IOS 기기에 모바일결제만 가능합니다.":"Apple Pay is available only for mobile payments on iOS devices."}function B(e){return(e??"KRW").trim().toUpperCase()||"KRW"}function at(e){return B(e)==="KRW"}function ot(e){const t=B(e);return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?`NHN KCP는 KRW 결제만 지원합니다. (${t})`:`NHN KCP supports KRW payments only. (${t})`}function it(e){const t=Math.max(0,Math.round(Number(e.amount??0))),n=Math.min(Math.max(0,Math.round(Number(e.tax_free_amount??0))),t);if(n<=0)return{};const r=Math.max(0,Math.round(Number(e.tax_amount??0))),a=Math.max(0,Math.round(Number(e.vat_amount??0))),o=Math.max(0,t-n),s=o>0&&r>0&&a>0?Math.min(o,Math.round(o*(a/r))):0,i=o-s;return{tax_flag:"TG03",comm_tax_mny:String(i),comm_vat_mny:String(s),comm_free_mny:String(n)}}class j extends Error{constructor(t){super(t??"결제가 취소되었습니다."),this.name="KcpCancelledError"}}const st={card:"100000000000",bank_transfer:"010000000000",virtual_account:"001000000000",mobile:"000010000000",bank:"010000000000",vbank:"001000000000",phone:"000010000000"},se={nhnkcp_payco:{payco_direct:"Y"},nhnkcp_naverpay:{naverpay_direct:"Y"},nhnkcp_naverpay_point:{naverpay_direct:"Y",naverpay_point_direct:"Y"},nhnkcp_kakaopay:{kakaopay_direct:"Y"},nhnkcp_applepay:{applepay_direct:"Y"}};function ct(e,t){const n=e.toLowerCase();return!t||!Object.prototype.hasOwnProperty.call(se,n)?{}:{param_opt_1:n,nhnkcp_easy_pay_method:n}}function dt(e){if(!e.isConnected)return!0;const t=window.getComputedStyle(e);if(t.display==="none"||t.visibility==="hidden")return!0;const n=e.getBoundingClientRect();return n.width<=0||n.height<=0}function lt(e,t){let n=!1,r,a;const o=()=>{n=!0,a&&(a.disconnect(),a=void 0),r!==void 0&&(window.clearInterval(r),r=void 0)},s=()=>{n||!dt(e)||(o(),t())},i=document.body??document.documentElement;return i&&typeof MutationObserver<"u"&&(a=new MutationObserver(s),a.observe(i,{attributes:!0,attributeFilter:["class","hidden","style"],childList:!0,subtree:!0})),r=window.setInterval(s,300),o}function ut(){if(typeof navigator>"u")return!1;if(K())return!0;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(navigator.userAgent||"").toLowerCase();return!!(/android|iphone|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||(navigator.maxTouchPoints??0)>1&&!t.includes("windows")&&!t.includes("macintosh"))}async function ce(e,t){const{pgPaymentData:n,paymentMethod:r}=e.params||{};if(!n){console.error("[sirsoft-pay_nhnkcp] pgPaymentData is required");return}const a=r??n.pay_method??"card",o=typeof a=="string"&&a.startsWith("nhnkcp_"),s=window.G7Core;let i=null;try{if(!at(n.currency)){const u=ot(n.currency);s?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:a}),s?.modal?.open?.("nhnkcp_payment_error_modal");return}if(I(a)&&!K()){const u=$();s?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:a}),s?.modal?.open?.("nhnkcp_payment_error_modal");return}const c=await s.api.get("/modules/sirsoft-ecommerce/payments/client-config/nhnkcp");if(!c.data){console.error("[sirsoft-pay_nhnkcpLine truncated
(function(){"use strict";function tt(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function nt(e){return e.trim().slice(0,160)}function rt(e){if(e)return e.replace(/\/payment\/close-report$/,"/payment/retry")}async function ce(e,t,n=!1){const r=window.G7Core?.api;if(typeof r?.post=="function"){await r.post(e,t);return}const a=await fetch(tt(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),keepalive:n});if(!a.ok){let o=`NHN KCP payment request failed (${a.status})`;try{const i=await a.json(),s=Array.isArray(i?.errors?.message)?i.errors.message[0]:i?.error??i?.message;typeof s=="string"&&s.trim()!==""&&(o=s)}catch{}throw new Error(o)}}async function at(e){const t=rt(e.closeReportUrl);t&&await ce(t,{oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??""})}async function ot(e,t="kcp-window-closed"){if(!e.closeReportUrl)return;const n={oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??"",reason:nt(t)||"kcp-window-closed"};try{await ce(e.closeReportUrl,n,!0)}catch(r){console.warn("[sirsoft-pay_nhnkcp] failed to report payment window close",r)}}const it="nhnkcp_applepay";function M(e){return e===it}function S(){if(typeof navigator>"u")return!1;const e=navigator,t=(e.userAgent||"").toLowerCase(),n=((e.userAgentData?.platform??e.platform)||"").toLowerCase(),r=e.maxTouchPoints??0;return/iphone|ipad|ipod/.test(t)||/iphone|ipad|ipod|ios/.test(n)?!0:/macintosh|mac os x/.test(t)&&r>1}function H(){return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?"애플페이는 IOS 기기에 모바일결제만 가능합니다.":"Apple Pay is available only for mobile payments on iOS devices."}function B(e){return(e??"KRW").trim().toUpperCase()||"KRW"}function st(e){return B(e)==="KRW"}function ct(e){const t=B(e);return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?`NHN KCP는 KRW 결제만 지원합니다. (${t})`:`NHN KCP supports KRW payments only. (${t})`}function dt(e){const t=Math.max(0,Math.round(Number(e.amount??0))),n=Math.min(Math.max(0,Math.round(Number(e.tax_free_amount??0))),t);if(n<=0)return{};const r=Math.max(0,Math.round(Number(e.tax_amount??0))),a=Math.max(0,Math.round(Number(e.vat_amount??0))),o=Math.max(0,t-n),i=o>0&&r>0&&a>0?Math.min(o,Math.round(o*(a/r))):0,s=o-i;return{tax_flag:"TG03",comm_tax_mny:String(s),comm_vat_mny:String(i),comm_free_mny:String(n)}}class D extends Error{constructor(t){super(t??"결제가 취소되었습니다."),this.name="KcpCancelledError"}}const lt={card:"100000000000",bank_transfer:"010000000000",virtual_account:"001000000000",mobile:"000010000000",bank:"010000000000",vbank:"001000000000",phone:"000010000000"},de={nhnkcp_payco:{payco_direct:"Y"},nhnkcp_naverpay:{naverpay_direct:"Y"},nhnkcp_naverpay_point:{naverpay_direct:"Y",naverpay_point_direct:"Y"},nhnkcp_kakaopay:{kakaopay_direct:"Y"},nhnkcp_applepay:{applepay_direct:"Y"}};function ut(e,t){const n=e.toLowerCase();return!t||!Object.prototype.hasOwnProperty.call(de,n)?{}:{param_opt_1:n,nhnkcp_easy_pay_method:n}}function pt(e){if(!e.isConnected)return!0;const t=window.getComputedStyle(e);if(t.display==="none"||t.visibility==="hidden")return!0;const n=e.getBoundingClientRect();return n.width<=0||n.height<=0}function mt(e,t){let n=!1,r,a;const o=()=>{n=!0,a&&(a.disconnect(),a=void 0),r!==void 0&&(window.clearInterval(r),r=void 0)},i=()=>{n||!pt(e)||(o(),t())},s=document.body??document.documentElement;return s&&typeof MutationObserver<"u"&&(a=new MutationObserver(i),a.observe(s,{attributes:!0,attributeFilter:["class","hidden","style"],childList:!0,subtree:!0})),r=window.setInterval(i,300),o}function yt(){if(typeof navigator>"u")return!1;if(S())return!0;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(navigator.userAgent||"").toLowerCase();return!!(/android|iphone|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||(navigator.maxTouchPoints??0)>1&&!t.includes("windows")&&!t.includes("macintosh"))}async function le(e,t){const{pgPaymentData:n,paymentMethod:r}=e.params||{};if(!n){console.error("[sirsoft-pay_nhnkcp] pgPaymentData is required");return}const a=r??n.pay_method??"card",o=typeof a=="string"&&a.startsWith("nhnkcp_"),i=window.G7Core;let s=null;try{if(!st(n.currency)){const u=ct(n.currency);i?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:a}),i?.modal?.open?.("nhnkcp_payment_error_modal");return}if(M(a)&&!S()){const u=H();i?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:a}),i?.modal?.open?.("nhnkcp_payment_error_modal");return}const c=await i.api.get("/modules/sirsoft-ecommerce/payments/client-config/nhnkcp");if(!c.data){console.error("[sirsoft-pay_nhnkcpLine truncated
<script src="${e.sdk_url}"><\/script>
</head><body style="margin:0;padding:0;background:transparent;">
<form name="order_info">${c}</form>
@@ -14,5 +14,5 @@ try {
window.__kcpFail && window.__kcpFail(e);
}
<\/script>
</body></html>`),N.close(),h=window.setTimeout(()=>{v(new Error("KCP SDK load timeout"))},15e3),f.__kcpReady=()=>{h!==void 0&&(clearTimeout(h),h=void 0)}})}const yt={nhnkcp_payco:"PAYCO",nhnkcp_naverpay:"네이버페이",nhnkcp_naverpay_point:"네이버페이 포인트",nhnkcp_kakaopay:"카카오페이",nhnkcp_applepay:"Apple Pay"},ft="0 0 0 2px #ffffff, 0 0 0 5px rgba(0,0,0,0.55)";function de(){return document.querySelector(".nhnkcp-easy-pay-btns")??null}function le(){const e=de();e&&e.querySelectorAll("button").forEach(t=>{t.style.boxShadow="",t.style.outline=""})}function ht(e){const t=de();if(!t)return;const n=yt[e];t.querySelectorAll("button").forEach(r=>{r.textContent?.trim()===n?(r.style.boxShadow=ft,r.style.outline="none"):(r.style.boxShadow="",r.style.outline="")})}function _t(e){const t=e.params?.paymentMethod;if(!t)return;const n=window.G7Core;if(I(t)&&!K()){const a=$();n?.state?.setLocal?.({paymentErrorMessage:a,isSubmittingOrder:!1}),n?.modal?.open?.("nhnkcp_payment_error_modal"),le();return}const r=typeof t=="string"&&t.startsWith("nhnkcp_");n?.state?.setLocal?.({paymentMethod:t,serverPaymentMethod:r?"card":t}),r?ht(t):le()}function gt(e){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly","readonly"),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.select();try{document.execCommand("copy")}finally{t.remove()}}async function wt(e){const t=String(e.params?.text??"");if(t!==""){if(navigator.clipboard&&typeof navigator.clipboard.writeText=="function"){await navigator.clipboard.writeText(t);return}gt(t)}}const ue={requestPayment:ce,setPaymentMethod:_t,copyToClipboard:wt},pe="/api/modules/sirsoft-ecommerce/user/orders",Pt="sirsoft-nhnkcp",D="sirsoft-pay_nhnkcp",me="nhnkcp_",k={info:(...e)=>console.info(`[${D}]`,...e),warn:(...e)=>console.warn(`[${D}]`,...e),error:(...e)=>console.error(`[${D}]`,...e)};function kt(e){return typeof e=="string"?e:e instanceof URL?e.toString():typeof Request<"u"&&e instanceof Request?e.url:String(e)}function bt(e,t){return t?.method?t.method.toUpperCase():typeof Request<"u"&&e instanceof Request?e.method.toUpperCase():"GET"}function Nt(e,t){if(t!=="POST")return!1;const n=e.split("?")[0].split("#")[0];return n===pe||n.endsWith(pe)}function Ct(){return window.location.pathname+window.location.search+window.location.hash}function xt(e,t){const n=JSON.stringify(t);return new Response(n,{status:e.status,statusText:e.statusText,headers:e.headers})}function vt(e){return new Response(JSON.stringify({success:!1,message:e,error:e,errors:{payment_method:[e]}}),{status:422,statusText:"Unprocessable Content",headers:{"Content-Type":"application/json"}})}function ye(e){try{return JSON.parse(e).payment_method}catch{return}}function Et(e,t){try{const n=JSON.parse(e);return n.payment_method=t,JSON.stringify(n)}catch{return e}}function At(){if(typeof window>"u"||typeof window.fetch!="function")return;const e="__sirsoftNhnkcpInterceptorInstalled",t=window;if(t[e])return;t[e]=!0;const n="__sirsoftPgOriginalFetch";t[n]||(t[n]=window.fetch.bind(window));const r=t[n],a=window.fetch.bind(window);window.fetch=async function(s,i){const c=kt(s),l=bt(s,i);if(!Nt(c,l))return a(s,i);let d,u=i;if(i?.body&&typeof i.body=="string"){const p=ye(i.body);typeof p=="string"&&p.startsWith(me)&&(d=p,u={...i,body:Et(i.body,"card")},k.info(`easy pay detected: replacing payment_method '${p}' → 'card'`))}if(!d)try{const p=window.G7Core?.state?.getLocal?.()?.paymentMethod;typeof p=="string"&&p.startsWith(me)&&(d=p,k.info(`easy pay detected from local state: '${p}'`))}catch{}if(I(d)&&!K()){const p=$();return k.warn(p),vt(p)}const f=await(d?r:a)(s,u);let x;try{x=f.clone()}catch{return f}let h=null;try{h=await x.json()}catch{return f}const m=h?.data;if(!m)return f;const oe=m.requires_pg_payment===!0,v=m.pg_provider===Pt;if(!d&&(!oe||!v)||d&&!m.order&&!m.pg_payment_data)return f;let w=m.pg_payment_data;if(!w&&d&&m.order){const p=m.order,g=p.options,P=g?.[0]?.product_name??String(p.order_number??""),T=(g?.length??0)>1?`${P} 외 ${(g?.length??0)-1}건`:P;w={order_number:p.order_number,order_name:T,amount:Math.floor(Number(p.total_due_amount??p.total_amount??0)),currency:"KRW",customer_name:p.orderer_name??null,customer_email:p.orderer_email??null,customer_phone:String(p.orderer_phone??"").replace(/[^0-9]/g,"")},k.info("pg_payment_data constructed from order (기본 PG가 타 PG)",{order_number:w.order_number,amount:w.amount})}if(!w)return k.warn("nhnkcp order detected but pg_payment_data missing"),f;k.info("intercepted order create response — opening PG popup");const N=m.order?{tax_amount:m.order.total_tax_amount??0,vat_amount:m.order.total_vat_amount??0,tax_free_amount:m.order.total_tax_free_amount??0}:{},_=!d&&u?.body&&typeof u.body=="string"?(()=>{const p=ye(u.body);return p?{...w,pay_method:p,...N}:{...w,...N}})():{...w,...N};ce({params:{pgPaymentData:_,paymentMethod:d}});const E={...h,data:{...m,requires_pg_payment:!1,redirect_url:Ct()}};Line truncated
</body></html>`),C.close(),h=window.setTimeout(()=>{v(new Error("KCP SDK load timeout"))},15e3),y.__kcpReady=()=>{h!==void 0&&(clearTimeout(h),h=void 0)}})}const _t={nhnkcp_payco:"PAYCO",nhnkcp_naverpay:"네이버페이",nhnkcp_naverpay_point:"네이버페이 포인트",nhnkcp_kakaopay:"카카오페이",nhnkcp_applepay:"Apple Pay"},gt="0 0 0 2px #ffffff, 0 0 0 5px rgba(0,0,0,0.55)";function ue(){return document.querySelector(".nhnkcp-easy-pay-btns")??null}function pe(){const e=ue();e&&e.querySelectorAll("button").forEach(t=>{t.style.boxShadow="",t.style.outline=""})}function wt(e){const t=ue();if(!t)return;const n=_t[e];t.querySelectorAll("button").forEach(r=>{r.textContent?.trim()===n?(r.style.boxShadow=gt,r.style.outline="none"):(r.style.boxShadow="",r.style.outline="")})}function Pt(e){const t=e.params?.paymentMethod;if(!t)return;const n=window.G7Core;if(M(t)&&!S()){const a=H();n?.state?.setLocal?.({paymentErrorMessage:a,isSubmittingOrder:!1}),n?.modal?.open?.("nhnkcp_payment_error_modal"),pe();return}const r=typeof t=="string"&&t.startsWith("nhnkcp_");n?.state?.setLocal?.({paymentMethod:t,serverPaymentMethod:r?"card":t}),r?wt(t):pe()}function kt(e){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly","readonly"),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.select();try{document.execCommand("copy")}finally{t.remove()}}async function bt(e){const t=String(e.params?.text??"");if(t!==""){if(navigator.clipboard&&typeof navigator.clipboard.writeText=="function"){await navigator.clipboard.writeText(t);return}kt(t)}}const me={requestPayment:le,setPaymentMethod:Pt,copyToClipboard:bt},ye="/api/modules/sirsoft-ecommerce/user/orders",Ct="sirsoft-nhnkcp",j="sirsoft-pay_nhnkcp",fe="nhnkcp_",b={info:(...e)=>console.info(`[${j}]`,...e),warn:(...e)=>console.warn(`[${j}]`,...e),error:(...e)=>console.error(`[${j}]`,...e)};function Nt(e){return typeof e=="string"?e:e instanceof URL?e.toString():typeof Request<"u"&&e instanceof Request?e.url:String(e)}function xt(e,t){return t?.method?t.method.toUpperCase():typeof Request<"u"&&e instanceof Request?e.method.toUpperCase():"GET"}function vt(e,t){if(t!=="POST")return!1;const n=e.split("?")[0].split("#")[0];return n===ye||n.endsWith(ye)}function Et(){return window.location.pathname+window.location.search+window.location.hash}function At(e,t){const n=JSON.stringify(t);return new Response(n,{status:e.status,statusText:e.statusText,headers:e.headers})}function St(e){return new Response(JSON.stringify({success:!1,message:e,error:e,errors:{payment_method:[e]}}),{status:422,statusText:"Unprocessable Content",headers:{"Content-Type":"application/json"}})}function he(e){try{return JSON.parse(e).payment_method}catch{return}}function Kt(e,t){try{const n=JSON.parse(e);return n.payment_method=t,JSON.stringify(n)}catch{return e}}function It(){if(typeof window>"u"||typeof window.fetch!="function")return;const e="__sirsoftNhnkcpInterceptorInstalled",t=window;if(t[e])return;t[e]=!0;const n="__sirsoftPgOriginalFetch";t[n]||(t[n]=window.fetch.bind(window));const r=t[n],a=window.fetch.bind(window);window.fetch=async function(i,s){const c=Nt(i),d=xt(i,s);if(!vt(c,d))return a(i,s);let l,u=s;if(s?.body&&typeof s.body=="string"){const p=he(s.body);typeof p=="string"&&p.startsWith(fe)&&(l=p,u={...s,body:Kt(s.body,"card")},b.info(`easy pay detected: replacing payment_method '${p}' → 'card'`))}if(!l)try{const p=window.G7Core?.state?.getLocal?.()?.paymentMethod;typeof p=="string"&&p.startsWith(fe)&&(l=p,b.info(`easy pay detected from local state: '${p}'`))}catch{}if(M(l)&&!S()){const p=H();return b.warn(p),St(p)}const y=await(l?r:a)(i,u);let x;try{x=y.clone()}catch{return y}let h=null;try{h=await x.json()}catch{return y}const f=h?.data;if(!f)return y;const se=f.requires_pg_payment===!0,v=f.pg_provider===Ct;if(!l&&(!se||!v)||l&&!f.order&&!f.pg_payment_data)return y;let w=f.pg_payment_data;if(!w&&l&&f.order){const p=f.order,g=p.options,k=g?.[0]?.product_name??String(p.order_number??""),I=(g?.length??0)>1?`${k} 외 ${(g?.length??0)-1}건`:k;w={order_number:p.order_number,order_name:I,amount:Math.floor(Number(p.total_due_amount??p.total_amount??0)),currency:"KRW",customer_name:p.orderer_name??null,customer_email:p.orderer_email??null,customer_phone:String(p.orderer_phone??"").replace(/[^0-9]/g,"")},b.info("pg_payment_data constructed from order (기본 PG가 타 PG)",{order_number:w.order_number,amount:w.amount})}if(!w)return b.warn("nhnkcp order detected but pg_payment_data missing"),y;b.info("intercepted order create response — opening PG popup");const C=f.order?{tax_amount:f.order.total_tax_amount??0,vat_amount:f.order.total_vat_amount??0,tax_free_amount:f.order.total_tax_free_amount??0}:{},_=!l&&u?.body&&typeof u.body=="string"?(()=>{const p=he(u.body);return p?{...w,pay_method:p,...C}:{...w,...C}})():{...w,...C};le({params:{pgPaymentData:_,paymentMethod:l}});const E={...h,data:{...f,requires_pg_payment:!1,redirect_url:Et()}};Line truncated
//# sourceMappingURL=plugin.iife.js.map
@@ -1 +1 @@
{"version":3,"file":"plugin.iife.js","sources":["../../resources/js/paymentCloseReport.ts","../../resources/js/support/applePayDevice.ts","../../resources/js/handlers/requestPayment.ts","../../resources/js/handlers/setPaymentMethod.ts","../../resources/js/handlers/copyToClipboard.ts","../../resources/js/handlers/index.ts","../../resources/js/orderResponseInterceptor.ts","../../resources/js/orderCompleteReceiptInjector.ts","../../resources/js/mypageOrderShowInjector.ts","../../resources/js/checkoutEasyPayInjector.ts","../../resources/js/adminApplePayNoticeInjector.ts","../../resources/js/adminPaymentMethodBrandInjector.ts","../../resources/js/index.ts"],"sourcesContent":["export interface PaymentCloseReportContext {\n closeReportUrl?: string;\n oid: string;\n price: number;\n buyer_email?: string;\n buyer_phone?: string;\n payment_method?: string;\n}\n\nfunction resolveApiUrl(url: string): string {\n if (/^https?:\\/\\//i.test(url) || url.startsWith('/api/')) {\n return url;\n }\n\n if (url.startsWith('/plugins/')) {\n return `/api${url}`;\n }\n\n if (url.startsWith('plugins/')) {\n return `/api/${url}`;\n }\n\n return url;\n}\n\nfunction trimReason(reason: string): string {\n return reason.trim().slice(0, 160);\n}\n\nfunction resolveRetryUrl(closeReportUrl?: string): string | undefined {\n if (!closeReportUrl) {\n return undefined;\n }\n\n return closeReportUrl.replace(/\\/payment\\/close-report$/, '/payment/retry');\n}\n\nasync function postPaymentContext(url: string, payload: Record<string, unknown>, keepalive = false): Promise<void> {\n const apiClient = ((window as any).G7Core)?.api;\n if (typeof apiClient?.post === 'function') {\n await apiClient.post(url, payload);\n return;\n }\n\n const response = await fetch(resolveApiUrl(url), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive,\n });\n\n if (!response.ok) {\n let message = `NHN KCP payment request failed (${response.status})`;\n try {\n const data = await response.json();\n const responseMessage = Array.isArray(data?.errors?.message)\n ? data.errors.message[0]\n : data?.error ?? data?.message;\n if (typeof responseMessage === 'string' && responseMessage.trim() !== '') {\n message = responseMessage;\n }\n } catch {\n // JSON 응답이 아니면 HTTP 상태 메시지를 사용한다.\n }\n throw new Error(message);\n }\n}\n\nexport async function preparePaymentRetry(context: PaymentCloseReportContext): Promise<void> {\n const retryUrl = resolveRetryUrl(context.closeReportUrl);\n if (!retryUrl) {\n return;\n }\n\n await postPaymentContext(retryUrl, {\n oid: context.oid,\n price: Number(context.price),\n buyer_email: context.buyer_email ?? '',\n buyer_phone: context.buyer_phone ?? '',\n payment_method: context.payment_method ?? '',\n });\n}\n\nexport async function reportPaymentWindowClosed(\n context: PaymentCloseReportContext,\n reason = 'kcp-window-closed',\n): Promise<void> {\n if (!context.closeReportUrl) {\n return;\n }\n\n const payload = {\n oid: context.oid,\n price: Number(context.price),\n buyer_email: context.buyer_email ?? '',\n buyer_phone: context.buyer_phone ?? '',\n payment_method: context.payment_method ?? '',\n reason: trimReason(reason) || 'kcp-window-closed',\n };\n\n try {\n await postPaymentContext(context.closeReportUrl, payload, true);\n } catch (error) {\n console.warn('[sirsoft-pay_nhnkcp] failed to report payment window close', error);\n }\n}\n","export const NHNKCP_APPLEPAY_METHOD = 'nhnkcp_applepay';\n\nexport function isNhnKcpApplePayMethod(paymentMethod: unknown): boolean {\n return paymentMethod === NHNKCP_APPLEPAY_METHOD;\n}\n\nexport function isIosMobileDevice(): boolean {\n if (typeof navigator === 'undefined') return false;\n\n const nav = navigator as Navigator & {\n userAgentData?: {\n mobile?: boolean;\n platform?: string;\n };\n maxTouchPoints?: number;\n };\n const ua = (nav.userAgent || '').toLowerCase();\n const platform = ((nav.userAgentData?.platform ?? nav.platform) || '').toLowerCase();\n const touchPoints = nav.maxTouchPoints ?? 0;\n\n if (/iphone|ipad|ipod/.test(ua) || /iphone|ipad|ipod|ios/.test(platform)) {\n return true;\n }\n\n return /macintosh|mac os x/.test(ua) && touchPoints > 1;\n}\n\nexport function applePayUnsupportedMessage(): string {\n const lang = typeof document !== 'undefined'\n ? document.documentElement.lang\n : '';\n\n if ((lang || '').toLowerCase().startsWith('ko')) {\n return '애플페이는 IOLine truncated
{"version":3,"file":"plugin.iife.js","sources":["../../resources/js/paymentCloseReport.ts","../../resources/js/support/applePayDevice.ts","../../resources/js/handlers/requestPayment.ts","../../resources/js/handlers/setPaymentMethod.ts","../../resources/js/handlers/copyToClipboard.ts","../../resources/js/handlers/index.ts","../../resources/js/orderResponseInterceptor.ts","../../resources/js/orderCompleteReceiptInjector.ts","../../resources/js/mypageOrderShowInjector.ts","../../resources/js/checkoutEasyPayInjector.ts","../../resources/js/adminApplePayNoticeInjector.ts","../../resources/js/adminPaymentMethodBrandInjector.ts","../../resources/js/adminOrderPaymentDisplayInjector.ts","../../resources/js/index.ts"],"sourcesContent":["export interface PaymentCloseReportContext {\n closeReportUrl?: string;\n oid: string;\n price: number;\n buyer_email?: string;\n buyer_phone?: string;\n payment_method?: string;\n}\n\nfunction resolveApiUrl(url: string): string {\n if (/^https?:\\/\\//i.test(url) || url.startsWith('/api/')) {\n return url;\n }\n\n if (url.startsWith('/plugins/')) {\n return `/api${url}`;\n }\n\n if (url.startsWith('plugins/')) {\n return `/api/${url}`;\n }\n\n return url;\n}\n\nfunction trimReason(reason: string): string {\n return reason.trim().slice(0, 160);\n}\n\nfunction resolveRetryUrl(closeReportUrl?: string): string | undefined {\n if (!closeReportUrl) {\n return undefined;\n }\n\n return closeReportUrl.replace(/\\/payment\\/close-report$/, '/payment/retry');\n}\n\nasync function postPaymentContext(url: string, payload: Record<string, unknown>, keepalive = false): Promise<void> {\n const apiClient = ((window as any).G7Core)?.api;\n if (typeof apiClient?.post === 'function') {\n await apiClient.post(url, payload);\n return;\n }\n\n const response = await fetch(resolveApiUrl(url), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive,\n });\n\n if (!response.ok) {\n let message = `NHN KCP payment request failed (${response.status})`;\n try {\n const data = await response.json();\n const responseMessage = Array.isArray(data?.errors?.message)\n ? data.errors.message[0]\n : data?.error ?? data?.message;\n if (typeof responseMessage === 'string' && responseMessage.trim() !== '') {\n message = responseMessage;\n }\n } catch {\n // JSON 응답이 아니면 HTTP 상태 메시지를 사용한다.\n }\n throw new Error(message);\n }\n}\n\nexport async function preparePaymentRetry(context: PaymentCloseReportContext): Promise<void> {\n const retryUrl = resolveRetryUrl(context.closeReportUrl);\n if (!retryUrl) {\n return;\n }\n\n await postPaymentContext(retryUrl, {\n oid: context.oid,\n price: Number(context.price),\n buyer_email: context.buyer_email ?? '',\n buyer_phone: context.buyer_phone ?? '',\n payment_method: context.payment_method ?? '',\n });\n}\n\nexport async function reportPaymentWindowClosed(\n context: PaymentCloseReportContext,\n reason = 'kcp-window-closed',\n): Promise<void> {\n if (!context.closeReportUrl) {\n return;\n }\n\n const payload = {\n oid: context.oid,\n price: Number(context.price),\n buyer_email: context.buyer_email ?? '',\n buyer_phone: context.buyer_phone ?? '',\n payment_method: context.payment_method ?? '',\n reason: trimReason(reason) || 'kcp-window-closed',\n };\n\n try {\n await postPaymentContext(context.closeReportUrl, payload, true);\n } catch (error) {\n console.warn('[sirsoft-pay_nhnkcp] failed to report payment window close', error);\n }\n}\n","export const NHNKCP_APPLEPAY_METHOD = 'nhnkcp_applepay';\n\nexport function isNhnKcpApplePayMethod(paymentMethod: unknown): boolean {\n return paymentMethod === NHNKCP_APPLEPAY_METHOD;\n}\n\nexport function isIosMobileDevice(): boolean {\n if (typeof navigator === 'undefined') return false;\n\n const nav = navigator as Navigator & {\n userAgentData?: {\n mobile?: boolean;\n platform?: string;\n };\n maxTouchPoints?: number;\n };\n const ua = (nav.userAgent || '').toLowerCase();\n const platform = ((nav.userAgentData?.platform ?? nav.platform) || '').toLowerCase();\n const touchPoints = nav.maxTouchPoints ?? 0;\n\n if (/iphone|ipad|ipod/.test(ua) || /iphone|ipad|ipod|ios/.test(platform)) {\n return true;\n }\n\n return /macintosh|mac os x/.test(ua) && touchPoints > 1;\n}\n\nexport function applePayUnsupportedMessage(): string {\n const lang = typeof document !== 'undefined'\n ? document.documentElement.lang\n : '';\n\n if ((lang || '').toLowerCase(Line truncated
@@ -12,6 +12,16 @@
"auth_required": true,
"loading_strategy": "progressive"
},
{
"id": "nhnkcp_easy_pay_display_map",
"label_key": "$t:sirsoft-pay_nhnkcp.editor.data_source.nhnkcp_easy_pay_display_map",
"type": "api",
"endpoint": "/api/plugins/sirsoft-pay_nhnkcp/admin/orders/easy-pay-display-map",
"method": "GET",
"auto_fetch": true,
"auth_required": true,
"loading_strategy": "progressive"
},
{
"id": "nicepay_test_map",
"label_key": "$t:sirsoft-pay_nhnkcp.editor.data_source.nicepay_test_map",
@@ -220,7 +230,7 @@
{
"type": "basic",
"name": "Span",
"text": "{{row.payment?.payment_method_label}}"
"text": "{{nhnkcp_easy_pay_display_map.data?.[row.order_number]?.payment_method_display_label ?? row.payment?.payment_method_label}}"
},
{
"type": "basic",
@@ -0,0 +1,44 @@
import { afterEach, describe, expect, it } from 'vitest';
import { patchAdminPaymentMethodDisplay } from '../adminOrderPaymentDisplayInjector';
import { patchMypagePaymentMethodDisplay } from '../mypageOrderShowInjector';
describe('NHN KCP order payment display injectors', () => {
afterEach(() => {
document.body.innerHTML = '';
});
it('마이페이지 주문 상세의 결제 방법 행을 간편결제 표시로 바꾼다', () => {
const container = document.createElement('div');
container.innerHTML = `
<div class="flex items-center justify-between">
<span class="text-gray-500">결제 방법</span>
<span class="text-gray-900">신용카드</span>
</div>
`;
expect(patchMypagePaymentMethodDisplay(container, '네이버페이 (신용카드)')).toBe(true);
expect(container.textContent?.replace(/\s+/g, '')).toContain('결제방법네이버페이(신용카드)');
expect(container.querySelector('[data-nhnkcp-payment-method-patched="true"]')).not.toBeNull();
});
it('관리자 주문 상세의 결제수단 행과 상단 배지를 간편결제 표시로 바꾼다', () => {
const root = document.createElement('div');
root.innerHTML = `
<span class="inline-flex rounded-full font-medium">신용카드</span>
<div>
<span class="text-xs block">결제수단</span>
<span class="text-sm font-semibold block">신용카드</span>
<span class="text-xs text-gray-500">(일시불)</span>
</div>
`;
expect(patchAdminPaymentMethodDisplay(root, {
_pay_method_label: '카카오페이 (신용카드)',
_base_pay_method_label: '신용카드',
_embedded_pg_provider_label: '카카오페이',
})).toBe(true);
const text = root.textContent?.replace(/\s+/g, '');
expect(text).toContain('카카오페이결제수단카카오페이(신용카드,일시불)');
});
});
@@ -0,0 +1,163 @@
const PLUGIN_ID = 'sirsoft-pay_nhnkcp';
const FLAG = '__nhnkcpAdminPaymentDisplayInjectorInstalled';
const ADMIN_ORDER_SHOW_RE = /^\/admin\/ecommerce\/orders\/([^/]+)$/;
interface AdminTransactionStatus {
_pay_method_label?: string | null;
_base_pay_method_label?: string | null;
_embedded_pg_provider_label?: string | null;
}
function getToken(): string | null {
return localStorage.getItem('auth_token');
}
async function fetchAdminTransactionStatus(orderNumber: string): Promise<AdminTransactionStatus | null> {
const token = getToken();
if (!token) return null;
try {
const res = await fetch(`/api/plugins/${PLUGIN_ID}/admin/orders/${orderNumber}/transaction-status`, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
});
if (!res.ok) return null;
const json = (await res.json()) as { data?: AdminTransactionStatus | null } | AdminTransactionStatus;
return ('data' in json ? json.data : json) ?? null;
} catch {
return null;
}
}
function directElementChildren(el: Element): HTMLElement[] {
return Array.from(el.children).filter(
(child): child is HTMLElement => child instanceof HTMLElement,
);
}
function patchLabeledPaymentMethod(root: Element, displayLabel: string): boolean {
let patched = false;
const rows = Array.from(root.querySelectorAll<HTMLElement>('div'));
for (const row of rows) {
const children = directElementChildren(row);
const labelIndex = children.findIndex(child => child.textContent?.trim() === '결제수단');
if (labelIndex < 0) continue;
const value = children
.slice(labelIndex + 1)
.find(child => child.textContent?.trim() && child.textContent.trim() !== '결제수단');
if (!value) continue;
const installment = children
.slice(labelIndex + 2)
.find(child => /^\(.+\)$/.test(child.textContent?.trim() ?? ''));
const installmentText = installment?.textContent?.trim().replace(/^\(|\)$/g, '') ?? '';
const nextDisplayLabel = installmentText && displayLabel.endsWith(')')
? displayLabel.replace(/\)$/, `, ${installmentText})`)
: displayLabel;
if (value.textContent?.trim() !== nextDisplayLabel) {
value.textContent = nextDisplayLabel;
value.dataset.nhnkcpPaymentMethodPatched = 'true';
}
row.dataset.nhnkcpPaymentMethodRow = 'admin-order';
if (installment && installmentText) {
installment.textContent = '';
installment.style.display = 'none';
installment.dataset.nhnkcpPaymentInstallmentHidden = 'true';
}
patched = true;
}
return patched;
}
function patchPaymentMethodBadge(root: Element, baseLabel: string, embeddedLabel: string): boolean {
const candidates = Array.from(root.querySelectorAll<HTMLElement>('span')).filter(
el => el.textContent?.trim() === baseLabel,
);
const badge = candidates.find(el => {
const className = typeof el.className === 'string' ? el.className : '';
return className.includes('rounded-full') || className.includes('font-medium');
});
if (!badge) return false;
badge.textContent = embeddedLabel;
badge.dataset.nhnkcpPaymentMethodPatched = 'true';
badge.dataset.nhnkcpPaymentMethodBadge = 'admin-order';
return true;
}
export function patchAdminPaymentMethodDisplay(
root: Element,
status: AdminTransactionStatus | null | undefined,
): boolean {
const displayLabel = status?._pay_method_label;
const embeddedLabel = status?._embedded_pg_provider_label;
const baseLabel = status?._base_pay_method_label;
if (!displayLabel || !embeddedLabel) return false;
const labeled = patchLabeledPaymentMethod(root, displayLabel);
const badge = baseLabel ? patchPaymentMethodBadge(root, baseLabel, embeddedLabel) : false;
return labeled || badge;
}
async function tryInject(orderNumber: string): Promise<boolean> {
const root = document.getElementById('section_payment_info') ?? document.getElementById('payment_info_card');
if (!root) return false;
const status = await fetchAdminTransactionStatus(orderNumber);
if (!status) return true;
const patched = patchAdminPaymentMethodDisplay(root, status);
if (patched) {
console.info(`[${PLUGIN_ID}] payment method display patched on admin order detail`);
}
return true;
}
function startPolling(orderNumber: string): void {
let attempts = 0;
const id = setInterval(() => {
attempts++;
void tryInject(orderNumber).then(done => {
if (done || attempts >= 30) clearInterval(id);
});
}, 400);
}
function onRouteChange(): void {
const match = location.pathname.match(ADMIN_ORDER_SHOW_RE);
if (match) startPolling(match[1]);
}
export function installAdminOrderPaymentDisplayInjector(): void {
if (typeof window === 'undefined') return;
const w = window as unknown as Record<string, unknown>;
if (w[FLAG]) return;
w[FLAG] = true;
console.info(`[${PLUGIN_ID}] admin order payment display injector installed`);
const schedule = (delay = 1500) => setTimeout(onRouteChange, delay);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => schedule());
} else {
schedule();
}
const origPush = history.pushState.bind(history);
history.pushState = (...args: Parameters<typeof history.pushState>) => {
origPush(...args);
schedule(600);
};
window.addEventListener('popstate', () => schedule(500));
}
@@ -5,6 +5,7 @@ import { installMypageOrderShowInjector } from './mypageOrderShowInjector';
import { installCheckoutEasyPayInjector } from './checkoutEasyPayInjector';
import { installAdminApplePayNoticeInjector } from './adminApplePayNoticeInjector';
import { installAdminPaymentMethodBrandInjector } from './adminPaymentMethodBrandInjector';
import { installAdminOrderPaymentDisplayInjector } from './adminOrderPaymentDisplayInjector';
class KcpReceiptPopup {
constructor(params: { url?: string; cash_url?: string }) {
@@ -100,6 +101,7 @@ installMypageOrderShowInjector();
installCheckoutEasyPayInjector();
installAdminApplePayNoticeInjector();
installAdminPaymentMethodBrandInjector();
installAdminOrderPaymentDisplayInjector();
initPlugin();
(window as Record<string, unknown>).__SirsoftNhnkcp = {
@@ -42,7 +42,13 @@ function getToken(): string | null {
return localStorage.getItem('auth_token');
}
async function fetchReceiptUrls(orderNumber: string): Promise<{ receipt_url?: string; cash_receipt_url?: string } | null> {
interface ReceiptInfo {
receipt_url?: string;
cash_receipt_url?: string | null;
payment_method_display_label?: string | null;
}
async function fetchReceiptUrls(orderNumber: string): Promise<ReceiptInfo | null> {
const token = getToken();
if (!token) return null;
try {
@@ -50,7 +56,7 @@ async function fetchReceiptUrls(orderNumber: string): Promise<{ receipt_url?: st
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
});
if (!res.ok) return null;
return (await res.json()) as { receipt_url?: string; cash_receipt_url?: string };
return (await res.json()) as ReceiptInfo;
} catch {
return null;
}
@@ -195,6 +201,34 @@ function makeInfoRow(label: string, value: string, valueClass = 'font-medium tex
return row;
}
export function patchMypagePaymentMethodDisplay(container: Element, displayLabel: string | null | undefined): boolean {
if (!displayLabel) return false;
const rows = Array.from(container.querySelectorAll<HTMLElement>('div'));
for (const row of rows) {
const spans = Array.from(row.children).filter(
(child): child is HTMLElement => child instanceof HTMLElement && child.tagName === 'SPAN',
);
if (spans.length < 2) continue;
const label = spans[0].textContent?.trim();
if (label !== '결제 방법' && label !== '결제수단' && label !== 'Payment Method') continue;
const value = spans[spans.length - 1];
if (value.textContent?.trim() === displayLabel) {
row.dataset.nhnkcpPaymentMethodRow = 'true';
return true;
}
value.textContent = displayLabel;
value.dataset.nhnkcpPaymentMethodPatched = 'true';
row.dataset.nhnkcpPaymentMethodRow = 'true';
return true;
}
return false;
}
function buildVbankInfoBlock(payment: Payment, totalAmountFormatted: string): HTMLElement {
const wrap = document.createElement('div');
wrap.id = VBANK_ID;
@@ -299,6 +333,12 @@ async function tryInject(orderNumber: string): Promise<boolean> {
const container = findPaymentRowsContainer();
if (!container) return false; // DOM 미렌더링 → 재시도
const receiptInfo = needsReceipt ? await fetchReceiptUrls(orderNumber) : null;
const displayPatched = patchMypagePaymentMethodDisplay(
container,
receiptInfo?.payment_method_display_label,
);
if (!document.getElementById(VBANK_ID) && needsVbank) {
container.appendChild(buildVbankInfoBlock(payment, orderData.total_amount_formatted ?? ''));
console.info(`[${PLUGIN_ID}] vbank info injected on mypage order show`);
@@ -311,6 +351,10 @@ async function tryInject(orderNumber: string): Promise<boolean> {
return false;
}
if (displayPatched) {
console.info(`[${PLUGIN_ID}] payment method display patched on mypage order show`);
}
if (!document.getElementById(MOCK_DEPOSIT_ID) && mightNeedMock) {
const info = await fetchMockDepositInfo(orderNumber);
if (info?.available) {
@@ -31,7 +31,13 @@ async function fetchPayment(orderNumber: string): Promise<Payment | null> {
}
}
async function fetchReceiptUrl(orderNumber: string): Promise<{ receipt_url: string; cash_receipt_url: string | null } | null> {
interface ReceiptInfo {
receipt_url: string;
cash_receipt_url: string | null;
payment_method_display_label?: string | null;
}
async function fetchReceiptUrl(orderNumber: string): Promise<ReceiptInfo | null> {
const token = getToken();
if (!token) return null;
@@ -40,7 +46,7 @@ async function fetchReceiptUrl(orderNumber: string): Promise<{ receipt_url: stri
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
});
if (!res.ok) return null;
return (await res.json()) as { receipt_url: string; cash_receipt_url: string | null };
return (await res.json()) as ReceiptInfo;
} catch {
return null;
}
@@ -55,6 +61,34 @@ function makeBtn(text: string, classes: string, onClick: (btn: HTMLButtonElement
return btn;
}
function patchPaymentMethodDisplay(displayLabel: string | null | undefined): boolean {
if (!displayLabel) return false;
const rows = Array.from(document.querySelectorAll<HTMLElement>('div'));
for (const row of rows) {
const spans = Array.from(row.children).filter(
(child): child is HTMLElement => child instanceof HTMLElement && child.tagName === 'SPAN',
);
if (spans.length < 2) continue;
const label = spans[0].textContent?.trim();
if (label !== '결제 방법' && label !== '결제수단' && label !== '결제 방식') continue;
const value = spans[spans.length - 1];
if (value.textContent?.trim() === displayLabel) {
row.dataset.nhnkcpPaymentMethodRow = 'true';
return true;
}
value.textContent = displayLabel;
value.dataset.nhnkcpPaymentMethodPatched = 'true';
row.dataset.nhnkcpPaymentMethodRow = 'true';
return true;
}
return false;
}
// 주문완료 페이지 버튼 영역에 영수증 버튼 주입
async function injectOnOrderComplete(orderNumber: string): Promise<void> {
if (document.getElementById(BTN_ID)) return;
@@ -70,6 +104,9 @@ async function injectOnOrderComplete(orderNumber: string): Promise<void> {
if (!blueBtn?.parentElement) return;
const receiptInfo = await fetchReceiptUrl(orderNumber);
patchPaymentMethodDisplay(receiptInfo?.payment_method_display_label);
const container = blueBtn.parentElement;
const receiptBtn = makeBtn('영수증 조회', blueBtn.className.replace(/bg-blue-\d+/g, 'bg-green-600').replace(/hover:bg-blue-\d+/g, 'hover:bg-green-700'), async (btn) => {
@@ -5,6 +5,7 @@
"kcp_escrow_delivery": "Escrow Delivery",
"kcp_status": "Payment Status",
"nhnkcp_easy_pay_config": "Easy Pay Config",
"nhnkcp_easy_pay_display_map": "Easy Pay Display Map",
"nhnkcp_test_map": "Test Payment Map",
"nicepay_test_map": "Test Payment Map",
"settings": "Payment Settings",
@@ -5,6 +5,7 @@
"kcp_escrow_delivery": "에스크로 배송",
"kcp_status": "결제 상태",
"nhnkcp_easy_pay_config": "간편결제 설정",
"nhnkcp_easy_pay_display_map": "간편결제 표시 맵",
"nhnkcp_test_map": "테스트 결제 맵",
"nicepay_test_map": "테스트 결제 맵",
"settings": "결제 설정",
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNhnkcp\Concerns;
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
trait ResolvesEasyPayDisplay
{
private const EASY_PAY_METHODS = [
'nhnkcp_payco' => [
'provider' => 'payco',
'label' => ['ko' => 'PAYCO (페이코)', 'en' => 'PAYCO'],
],
'nhnkcp_naverpay' => [
'provider' => 'naverpay',
'label' => ['ko' => '네이버페이', 'en' => 'NaverPay'],
],
'nhnkcp_naverpay_point' => [
'provider' => 'naverpay_point',
'label' => ['ko' => '네이버페이 포인트결제', 'en' => 'NaverPay Point'],
],
'nhnkcp_kakaopay' => [
'provider' => 'kakaopay',
'label' => ['ko' => '카카오페이', 'en' => 'KakaoPay'],
],
'nhnkcp_applepay' => [
'provider' => 'applepay',
'label' => ['ko' => '애플페이', 'en' => 'Apple Pay'],
],
];
/**
* @return array{method: string, provider: string, label: array{ko: string, en: string}}|array{}
*/
protected function resolveEasyPayMeta(array $payload): array
{
$method = strtolower(trim((string) ($payload['nhnkcp_easy_pay_method'] ?? $payload['param_opt_1'] ?? '')));
if (! isset(self::EASY_PAY_METHODS[$method])) {
return [];
}
$definition = self::EASY_PAY_METHODS[$method];
return [
'method' => $method,
'provider' => $definition['provider'],
'label' => $definition['label'],
];
}
/**
* @return array<string, mixed>
*/
protected function decodePaymentMeta(mixed $paymentMeta): array
{
if (is_array($paymentMeta)) {
return $paymentMeta;
}
if (! is_string($paymentMeta) || trim($paymentMeta) === '') {
return [];
}
$decoded = json_decode($paymentMeta, true);
return is_array($decoded) ? $decoded : [];
}
/**
* @return array{
* payment_method_label: string|null,
* payment_method_display_label: string|null,
* selected_payment_method: string|null,
* embedded_pg_provider: string|null,
* embedded_pg_provider_label: string|null
* }
*/
protected function resolvePaymentDisplay(object $payment): array
{
$paymentMeta = $this->decodePaymentMeta($payment->payment_meta ?? null);
$selectedPaymentMethod = $this->stringOrNull($paymentMeta['nhnkcp_easy_pay_method'] ?? null);
$provider = $this->stringOrNull(
$payment->embedded_pg_provider
?? $paymentMeta['nhnkcp_easy_pay_provider']
?? $paymentMeta['embedded_pg_provider']
?? null
);
if ($provider === null && $selectedPaymentMethod !== null) {
$provider = $this->stringOrNull(self::EASY_PAY_METHODS[$selectedPaymentMethod]['provider'] ?? null);
}
$baseLabel = $this->paymentMethodLabel($this->stringOrNull($payment->payment_method ?? null));
$embeddedLabel = $this->embeddedPgProviderLabel($provider, $paymentMeta);
return [
'payment_method_label' => $baseLabel,
'payment_method_display_label' => $this->paymentMethodDisplayLabel($baseLabel, $embeddedLabel),
'selected_payment_method' => $selectedPaymentMethod,
'embedded_pg_provider' => $provider,
'embedded_pg_provider_label' => $embeddedLabel,
];
}
protected function paymentMethodLabel(?string $method): ?string
{
if ($method === null || $method === '') {
return null;
}
return PaymentMethodEnum::tryFrom($method)?->label() ?? $method;
}
protected function paymentMethodDisplayLabel(?string $baseLabel, ?string $embeddedLabel): ?string
{
if ($baseLabel === null || $baseLabel === '') {
return $embeddedLabel;
}
if ($embeddedLabel === null || $embeddedLabel === '') {
return $baseLabel;
}
return $embeddedLabel . ' (' . $baseLabel . ')';
}
/**
* @param array<string, mixed> $paymentMeta
*/
protected function embeddedPgProviderLabel(?string $provider, array $paymentMeta = []): ?string
{
$metaLabel = $this->localizedLabel($paymentMeta['nhnkcp_easy_pay_label'] ?? null)
?? $this->localizedLabel($paymentMeta['embedded_pg_provider_label'] ?? null);
if ($metaLabel !== null) {
return $metaLabel;
}
if ($provider === null || $provider === '') {
return null;
}
foreach (self::EASY_PAY_METHODS as $definition) {
if (($definition['provider'] ?? null) === $provider) {
return $this->localizedLabel($definition['label'] ?? null);
}
}
return null;
}
private function localizedLabel(mixed $label): ?string
{
if (is_string($label) && trim($label) !== '') {
return $label;
}
if (! is_array($label)) {
return null;
}
$locale = app()->getLocale();
return $this->stringOrNull($label[$locale] ?? null)
?? $this->stringOrNull($label['ko'] ?? null)
?? $this->stringOrNull($label['en'] ?? null);
}
private function stringOrNull(mixed $value): ?string
{
if (! is_string($value)) {
return null;
}
$value = trim($value);
return $value === '' ? null : $value;
}
}
@@ -9,9 +9,12 @@ use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Services\PluginSettingsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay;
class AdminOrderListController extends AdminBaseController
{
use ResolvesEasyPayDisplay;
private const PLUGIN_IDENTIFIER = 'sirsoft-pay_nhnkcp';
private bool $isTest;
@@ -58,4 +61,44 @@ class AdminOrderListController extends AdminBaseController
return ResponseHelper::success('messages.success', $map);
}
/**
* NHN KCP 간편결제 표시 맵 반환
*
* 관리자 주문목록은 코어 OrderPaymentResource 의 기본 payment_method_label 을 사용하므로,
* 플러그인 데이터소스로 간편결제 원 결제수단 표시명을 보강한다.
*
* @return JsonResponse 주문번호별 표시 맵
*/
public function easyPayDisplayMap(): JsonResponse
{
$rows = DB::table('ecommerce_orders as o')
->join('ecommerce_order_payments as p', 'p.order_id', '=', 'o.id')
->where('p.pg_provider', 'nhnkcp')
->where('p.created_at', '>=', now()->subMonths(6))
->select([
'o.order_number',
'p.payment_method',
'p.embedded_pg_provider',
'p.payment_meta',
])
->get();
$map = [];
foreach ($rows as $row) {
$display = $this->resolvePaymentDisplay($row);
if (($display['embedded_pg_provider_label'] ?? null) === null) {
continue;
}
$map[$row->order_number] = [
'payment_method_label' => $display['payment_method_label'],
'payment_method_display_label' => $display['payment_method_display_label'],
'embedded_pg_provider' => $display['embedded_pg_provider'],
'embedded_pg_provider_label' => $display['embedded_pg_provider_label'],
];
}
return ResponseHelper::success('messages.success', $map);
}
}
@@ -9,6 +9,7 @@ use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Services\PluginSettingsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay;
/**
* NHN KCP 거래 정보 관리자 컨트롤러
@@ -18,6 +19,8 @@ use Illuminate\Support\Facades\DB;
*/
class AdminTransactionController extends AdminBaseController
{
use ResolvesEasyPayDisplay;
private const PLUGIN_IDENTIFIER = 'sirsoft-pay_nhnkcp';
public function __construct(
@@ -46,7 +49,7 @@ class AdminTransactionController extends AdminBaseController
->where('p.pg_provider', 'nhnkcp')
->whereNotNull('p.transaction_id')
->where('p.transaction_id', '!=', '')
->select(['p.transaction_id', 'p.payment_meta', 'p.payment_method'])
->select(['p.transaction_id', 'p.payment_meta', 'p.payment_method', 'p.embedded_pg_provider'])
->first();
if (! $payment) {
@@ -56,8 +59,9 @@ class AdminTransactionController extends AdminBaseController
$settings = $this->pluginSettingsService->get(self::PLUGIN_IDENTIFIER) ?? [];
$isTest = (bool) ($settings['is_test_mode'] ?? true);
$meta = $payment->payment_meta ? json_decode($payment->payment_meta, true) : [];
$meta = $this->decodePaymentMeta($payment->payment_meta ?? null);
$rawResponse = $meta['pg_raw_response'] ?? [];
$display = $this->resolvePaymentDisplay($payment);
return ResponseHelper::success('messages.success', [
'tno' => $payment->transaction_id,
@@ -69,6 +73,11 @@ class AdminTransactionController extends AdminBaseController
'account' => $meta['account'] ?? null,
'bank_name' => $rawResponse['bank_name'] ?? $meta['bank_name'] ?? null,
'_is_test_mode' => $isTest,
'_base_pay_method_label' => $display['payment_method_label'],
'_embedded_pg_provider' => $display['embedded_pg_provider'],
'_embedded_pg_provider_label' => $display['embedded_pg_provider_label'],
'_pay_method_label' => $display['payment_method_display_label'],
'payment_method_display_label' => $display['payment_method_display_label'],
]);
}
}
@@ -19,6 +19,7 @@ use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService;
use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService;
use Plugins\Sirsoft\PayNhnkcp\Concerns\PreventsReplayCallback;
use Plugins\Sirsoft\PayNhnkcp\Concerns\RecordsPaymentWindowClosure;
use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay;
use Plugins\Sirsoft\PayNhnkcp\Concerns\SanitizesPgResponse;
use Plugins\Sirsoft\PayNhnkcp\Concerns\SendsKcpNotifyResponse;
use Plugins\Sirsoft\PayNhnkcp\Http\Requests\AuthCallbackRequest;
@@ -36,6 +37,7 @@ class PaymentCallbackController
{
use PreventsReplayCallback;
use RecordsPaymentWindowClosure;
use ResolvesEasyPayDisplay;
use SanitizesPgResponse;
use SendsKcpNotifyResponse;
@@ -102,29 +104,6 @@ class PaymentCallbackController
'use_pay_method',
];
private const EASY_PAY_METHODS = [
'nhnkcp_payco' => [
'provider' => 'payco',
'label' => ['ko' => 'PAYCO (페이코)', 'en' => 'PAYCO'],
],
'nhnkcp_naverpay' => [
'provider' => 'naverpay',
'label' => ['ko' => '네이버페이', 'en' => 'NaverPay'],
],
'nhnkcp_naverpay_point' => [
'provider' => 'naverpay_point',
'label' => ['ko' => '네이버페이 포인트결제', 'en' => 'NaverPay Point'],
],
'nhnkcp_kakaopay' => [
'provider' => 'kakaopay',
'label' => ['ko' => '카카오페이', 'en' => 'KakaoPay'],
],
'nhnkcp_applepay' => [
'provider' => 'applepay',
'label' => ['ko' => '애플페이', 'en' => 'Apple Pay'],
],
];
public function __construct(
private readonly OrderProcessingService $orderService,
private readonly PluginSettingsService $pluginSettingsService,
@@ -786,25 +765,6 @@ class PaymentCallbackController
];
}
/**
* @return array{method: string, provider: string, label: array{ko: string, en: string}}|array{}
*/
private function resolveEasyPayMeta(array $payload): array
{
$method = strtolower(trim((string) ($payload['nhnkcp_easy_pay_method'] ?? $payload['param_opt_1'] ?? '')));
if (! isset(self::EASY_PAY_METHODS[$method])) {
return [];
}
$definition = self::EASY_PAY_METHODS[$method];
return [
'method' => $method,
'provider' => $definition['provider'],
'label' => $definition['label'],
];
}
private function resolveSuccessUrl(string $orderId): string
{
$settings = $this->pluginSettingsService->get(self::PLUGIN_IDENTIFIER) ?? [];
@@ -8,10 +8,13 @@ use App\Services\PluginSettingsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay;
use Plugins\Sirsoft\PayNhnkcp\Services\NhnKcpApiService;
class UserReceiptController
{
use ResolvesEasyPayDisplay;
private const PLUGIN_IDENTIFIER = 'sirsoft-pay_nhnkcp';
// 카드/계좌이체 영수증 URL (cmd= 로 끝나므로 cmd 값을 바로 붙임)
@@ -56,6 +59,7 @@ class UserReceiptController
'p.transaction_id',
'p.payment_meta',
'p.payment_method',
'p.embedded_pg_provider',
'p.payment_device',
'p.paid_amount_local',
'o.order_number',
@@ -76,6 +80,7 @@ class UserReceiptController
$orderNo = $payment->order_number;
$tradeMony = (int) round((float) ($payment->paid_amount_local ?? 0));
$method = $payment->payment_method ?? '';
$display = $this->resolvePaymentDisplay($payment);
// 결제수단 분류 — KCP 영수증 cmd 는 결제수단(휴대폰결제 vs 그 외) 으로만 분기.
// 디바이스(PC/모바일) 와 무관 — 모바일 디바이스에서 카드결제해도 card_bill 로 가야 함.
@@ -93,7 +98,7 @@ class UserReceiptController
// 현금영수증 URL (계좌이체 · 가상계좌만 해당)
$cashReceiptUrl = null;
if ($isCashMethod) {
$meta = $payment->payment_meta ? json_decode($payment->payment_meta, true) : [];
$meta = $this->decodePaymentMeta($payment->payment_meta ?? null);
$pgRaw = $meta['pg_raw_response'] ?? $meta;
$authNo = $pgRaw['app_no'] ?? $pgRaw['receipt_no'] ?? $tno;
$siteCd = $this->apiService->getSiteCd();
@@ -108,6 +113,11 @@ class UserReceiptController
'receipt_url' => $receiptUrl,
'cash_receipt_url' => $cashReceiptUrl,
'is_test_mode' => $isTest,
'payment_method_label' => $display['payment_method_label'],
'payment_method_display_label' => $display['payment_method_display_label'],
'selected_payment_method' => $display['selected_payment_method'],
'embedded_pg_provider' => $display['embedded_pg_provider'],
'embedded_pg_provider_label' => $display['embedded_pg_provider_label'],
]);
}
}
@@ -14,8 +14,12 @@ class EnsureAdminOrderListTestBadgeLayoutListener implements HookListenerInterfa
private const DATA_SOURCE_ID = 'nhnkcp_test_map';
private const EASY_PAY_DISPLAY_DATA_SOURCE_ID = 'nhnkcp_easy_pay_display_map';
private const BADGE_CONDITION = 'nhnkcp_test_map.data?.[row.order_number] === true';
private const EASY_PAY_DISPLAY_EXPRESSION = 'nhnkcp_easy_pay_display_map.data?.[row.order_number]?.payment_method_display_label ?? row.payment?.payment_method_label';
/**
* @return array<string, array<string, mixed>>
*/
@@ -65,24 +69,38 @@ class EnsureAdminOrderListTestBadgeLayoutListener implements HookListenerInterfa
? $layout['data_sources']
: [];
$existingIds = [];
foreach ($dataSources as $dataSource) {
if (is_array($dataSource) && ($dataSource['id'] ?? null) === self::DATA_SOURCE_ID) {
$layout['data_sources'] = $dataSources;
return $layout;
if (is_array($dataSource) && is_string($dataSource['id'] ?? null)) {
$existingIds[$dataSource['id']] = true;
}
}
$dataSources[] = [
'id' => self::DATA_SOURCE_ID,
'label_key' => '$t:sirsoft-pay_nhnkcp.editor.data_source.nhnkcp_test_map',
'type' => 'api',
'endpoint' => '/api/plugins/sirsoft-pay_nhnkcp/admin/orders/test-mode-map',
'method' => 'GET',
'auto_fetch' => true,
'auth_required' => true,
'loading_strategy' => 'progressive',
];
if (! isset($existingIds[self::DATA_SOURCE_ID])) {
$dataSources[] = [
'id' => self::DATA_SOURCE_ID,
'label_key' => '$t:sirsoft-pay_nhnkcp.editor.data_source.nhnkcp_test_map',
'type' => 'api',
'endpoint' => '/api/plugins/sirsoft-pay_nhnkcp/admin/orders/test-mode-map',
'method' => 'GET',
'auto_fetch' => true,
'auth_required' => true,
'loading_strategy' => 'progressive',
];
}
if (! isset($existingIds[self::EASY_PAY_DISPLAY_DATA_SOURCE_ID])) {
$dataSources[] = [
'id' => self::EASY_PAY_DISPLAY_DATA_SOURCE_ID,
'label_key' => '$t:sirsoft-pay_nhnkcp.editor.data_source.nhnkcp_easy_pay_display_map',
'type' => 'api',
'endpoint' => '/api/plugins/sirsoft-pay_nhnkcp/admin/orders/easy-pay-display-map',
'method' => 'GET',
'auto_fetch' => true,
'auth_required' => true,
'loading_strategy' => 'progressive',
];
}
$layout['data_sources'] = $dataSources;
@@ -130,6 +148,7 @@ class EnsureAdminOrderListTestBadgeLayoutListener implements HookListenerInterfa
continue;
}
$this->ensurePaymentMethodDisplayExpression($column);
$this->appendBadgeToColumn($column);
return;
@@ -137,6 +156,27 @@ class EnsureAdminOrderListTestBadgeLayoutListener implements HookListenerInterfa
unset($column);
}
/**
* @param array<string|int, mixed> $node
*/
private function ensurePaymentMethodDisplayExpression(array &$node): bool
{
if (($node['text'] ?? null) === '{{row.payment?.payment_method_label}}') {
$node['text'] = '{{'.self::EASY_PAY_DISPLAY_EXPRESSION.'}}';
return true;
}
foreach ($node as &$value) {
if (is_array($value) && $this->ensurePaymentMethodDisplayExpression($value)) {
return true;
}
}
unset($value);
return false;
}
/**
* @param array<string, mixed> $column
*/
@@ -56,6 +56,10 @@ Route::prefix('admin')->name('admin.')->middleware(['auth:sanctum', 'admin'])->g
Route::get('/orders/test-mode-map', [AdminOrderListController::class, 'testModeMap'])
->name('orders.test-mode-map');
// 간편결제 원 결제수단 표시 맵 (관리자 주문목록 보강용)
Route::get('/orders/easy-pay-display-map', [AdminOrderListController::class, 'easyPayDisplayMap'])
->name('orders.easy-pay-display-map');
// 주문번호로 거래 정보 조회 (레이아웃 확장 자동 로드용)
Route::get('/orders/{orderNumber}/transaction-status', [AdminTransactionController::class, 'queryByOrder'])
->name('orders.transaction-status');
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNhnkcp\Tests\Feature\Controllers;
use App\Models\User;
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderFactory;
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderPaymentFactory;
use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
use Plugins\Sirsoft\PayNhnkcp\Tests\PluginTestCase;
class AdminOrderListControllerTest extends PluginTestCase
{
public function test_easy_pay_display_map_returns_order_payment_display_labels(): void
{
app()->setLocale('ko');
$admin = $this->createAdminUser(['sirsoft-ecommerce.orders.read']);
$order = OrderFactory::new()->create([
'user_id' => User::factory()->create()->id,
'order_number' => 'ORD-KCP-LIST-' . random_int(10000, 99999),
'order_status' => OrderStatusEnum::PAYMENT_COMPLETE,
'total_amount' => 50000,
'total_due_amount' => 0,
'total_paid_amount' => 50000,
'paid_at' => now(),
]);
OrderPaymentFactory::new()->create([
'order_id' => $order->id,
'payment_status' => PaymentStatusEnum::PAID,
'payment_method' => PaymentMethodEnum::CARD,
'pg_provider' => 'nhnkcp',
'transaction_id' => 'KCP_TNO_PAYCO',
'embedded_pg_provider' => 'payco',
'paid_amount_local' => 50000,
'paid_at' => now(),
'payment_meta' => [
'nhnkcp_easy_pay_method' => 'nhnkcp_payco',
'nhnkcp_easy_pay_provider' => 'payco',
'nhnkcp_easy_pay_label' => [
'ko' => 'PAYCO (페이코)',
'en' => 'PAYCO',
],
],
]);
$response = $this->actingAs($admin)
->getJson('/api/plugins/sirsoft-pay_nhnkcp/admin/orders/easy-pay-display-map');
$response->assertOk();
$display = $response->json("data.{$order->order_number}");
$this->assertContains($display['embedded_pg_provider_label'] ?? null, ['PAYCO (페이코)', 'PAYCO']);
$this->assertSame(
($display['embedded_pg_provider_label'] ?? '') . ' (' . ($display['payment_method_label'] ?? '') . ')',
$display['payment_method_display_label'] ?? null,
);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNhnkcp\Tests\Feature\Controllers;
use App\Models\User;
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderFactory;
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderPaymentFactory;
use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
use Plugins\Sirsoft\PayNhnkcp\Tests\PluginTestCase;
class AdminTransactionControllerTest extends PluginTestCase
{
public function test_transaction_status_includes_easy_pay_display_label(): void
{
app()->setLocale('ko');
$admin = $this->createAdminUser(['sirsoft-ecommerce.orders.read']);
$order = OrderFactory::new()->create([
'user_id' => User::factory()->create()->id,
'order_number' => 'ORD-KCP-ADMIN-' . random_int(10000, 99999),
'order_status' => OrderStatusEnum::PAYMENT_COMPLETE,
'total_amount' => 50000,
'total_due_amount' => 0,
'total_paid_amount' => 50000,
'paid_at' => now(),
]);
OrderPaymentFactory::new()->create([
'order_id' => $order->id,
'payment_status' => PaymentStatusEnum::PAID,
'payment_method' => PaymentMethodEnum::CARD,
'pg_provider' => 'nhnkcp',
'transaction_id' => 'KCP_TNO_KAKAOPAY',
'embedded_pg_provider' => 'kakaopay',
'paid_amount_local' => 50000,
'paid_at' => now(),
'payment_meta' => [
'nhnkcp_easy_pay_method' => 'nhnkcp_kakaopay',
'nhnkcp_easy_pay_provider' => 'kakaopay',
'nhnkcp_easy_pay_label' => [
'ko' => '카카오페이',
'en' => 'KakaoPay',
],
'pg_raw_response' => [
'app_no' => 'APP12345',
'use_pay_method' => 'CARD',
],
],
]);
$response = $this->actingAs($admin)
->getJson("/api/plugins/sirsoft-pay_nhnkcp/admin/orders/{$order->order_number}/transaction-status");
$response->assertOk()
->assertJsonPath('data._embedded_pg_provider', 'kakaopay');
$data = $response->json('data');
$this->assertContains($data['_embedded_pg_provider_label'] ?? null, ['카카오페이', 'KakaoPay']);
$this->assertSame(
($data['_embedded_pg_provider_label'] ?? '') . ' (' . ($data['_base_pay_method_label'] ?? '') . ')',
$data['_pay_method_label'] ?? null,
);
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNhnkcp\Tests\Feature\Controllers;
use App\Models\User;
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderFactory;
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderPaymentFactory;
use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
use Plugins\Sirsoft\PayNhnkcp\Tests\PluginTestCase;
class UserReceiptControllerTest extends PluginTestCase
{
public function test_receipt_response_includes_easy_pay_display_label(): void
{
app()->setLocale('ko');
$user = User::factory()->create();
$order = OrderFactory::new()->create([
'user_id' => $user->id,
'order_number' => 'ORD-KCP-RECEIPT-' . random_int(10000, 99999),
'order_status' => OrderStatusEnum::PAYMENT_COMPLETE,
'total_amount' => 50000,
'total_due_amount' => 0,
'total_paid_amount' => 50000,
'paid_at' => now(),
]);
OrderPaymentFactory::new()->create([
'order_id' => $order->id,
'payment_status' => PaymentStatusEnum::PAID,
'payment_method' => PaymentMethodEnum::CARD,
'pg_provider' => 'nhnkcp',
'transaction_id' => 'KCP_TNO_NAVERPAY',
'embedded_pg_provider' => 'naverpay',
'paid_amount_local' => 50000,
'paid_at' => now(),
'payment_meta' => [
'nhnkcp_easy_pay_method' => 'nhnkcp_naverpay',
'nhnkcp_easy_pay_provider' => 'naverpay',
'nhnkcp_easy_pay_label' => [
'ko' => '네이버페이',
'en' => 'NaverPay',
],
],
]);
$response = $this->actingAs($user)
->getJson("/api/plugins/sirsoft-pay_nhnkcp/user/orders/{$order->order_number}/receipt");
$response->assertOk()
->assertJsonPath('selected_payment_method', 'nhnkcp_naverpay')
->assertJsonPath('embedded_pg_provider', 'naverpay');
$data = $response->json();
$this->assertContains($data['embedded_pg_provider_label'] ?? null, ['네이버페이', 'NaverPay']);
$this->assertSame(
($data['embedded_pg_provider_label'] ?? '') . ' (' . ($data['payment_method_label'] ?? '') . ')',
$data['payment_method_display_label'] ?? null,
);
}
}
@@ -18,9 +18,12 @@ class EnsureAdminOrderLayoutExtensionsListenerTest extends TestCase
$json = json_encode($result, JSON_UNESCAPED_SLASHES);
$this->assertSame(1, $this->countDataSources($result, 'nhnkcp_test_map'));
$this->assertSame(1, $this->countDataSources($result, 'nhnkcp_easy_pay_display_map'));
$this->assertSame(1, $this->countString($result, 'nhnkcp_test_map.data?.[row.order_number] === true'));
$this->assertSame(1, $this->countString($result, 'nhnkcp_easy_pay_display_map.data?.[row.order_number]?.payment_method_display_label ?? row.payment?.payment_method_label'));
$this->assertIsString($json);
$this->assertStringContainsString('/api/plugins/sirsoft-pay_nhnkcp/admin/orders/test-mode-map', $json);
$this->assertStringContainsString('/api/plugins/sirsoft-pay_nhnkcp/admin/orders/easy-pay-display-map', $json);
$this->assertStringContainsString('sirsoft-pay_nhnkcp.admin.test_mode_badge', $json);
}
@@ -32,7 +35,9 @@ class EnsureAdminOrderLayoutExtensionsListenerTest extends TestCase
$twice = $listener->ensureTestBadgeLayout($once, 1);
$this->assertSame(1, $this->countDataSources($twice, 'nhnkcp_test_map'));
$this->assertSame(1, $this->countDataSources($twice, 'nhnkcp_easy_pay_display_map'));
$this->assertSame(1, $this->countString($twice, 'nhnkcp_test_map.data?.[row.order_number] === true'));
$this->assertSame(1, $this->countString($twice, 'nhnkcp_easy_pay_display_map.data?.[row.order_number]?.payment_method_display_label ?? row.payment?.payment_method_label'));
}
public function test_detail_listener_ensures_test_notice_and_payment_panels(): void