├── .babelrc ├── .gitignore ├── .npmignore ├── Dockerfile ├── Gemfile ├── README.md ├── Rakefile ├── compile.sh ├── config ├── bootpay_inapp.example.service └── bootpay_serve_js.example.service ├── docker-compose.yml ├── html └── template.html ├── lib ├── bootpay.coffee ├── event.coffee ├── extend │ ├── analytics.coffee │ ├── bootpay_event.coffee │ ├── common.coffee │ ├── encrypt.coffee │ ├── message.coffee │ ├── notification.coffee │ ├── payment.coffee │ ├── platform.coffee │ └── storage.coffee ├── logger.coffee └── style.coffee ├── package.json ├── prepublish.sh ├── public └── waiting │ ├── index.html │ └── stylesheets │ └── style.css ├── test.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.iml 3 | *.idea 4 | /public/* 5 | !/public/waiting 6 | 7 | package-lock.json 8 | /node_modules 9 | yarn.lock 10 | lib/*.js 11 | lib/extend/*.js 12 | /test 13 | /html/test.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.idea 3 | .DS_Store 4 | .git* 5 | .idea 6 | Makefile 7 | docs/ 8 | examples/ 9 | support/ 10 | node_modules/ 11 | test/ 12 | .gitignore 13 | package-lock.json 14 | yarn.lock 15 | Gemfile.lock 16 | lib/*.coffee -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.7.8 2 | 3 | RUN echo "$(sed -e 's/deb.debian.org/ftp.kr.debian.org/g' /etc/apt/sources.list)" > /etc/apt/sources.list 4 | # Install dependencies 5 | RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \ 6 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ 7 | echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 8 | RUN apt-get update && apt-get install -y build-essential libpq-dev nginx-extras imagemagick libmagickwand-dev libssl-dev libreadline-dev nodejs libffi-dev yarn vim 9 | RUN apt-get clean && rm -rf /var/cache/apt/archives && rm -rf /var/lib/apt/lists 10 | 11 | # Set an environment variable where the Rails app is installed to inside of Docker image: 12 | ENV RAILS_ROOT /var/www 13 | # RUN mkdir -p $RAILS_ROOT 14 | 15 | # Set working directory, where the commands will be ran: 16 | WORKDIR $RAILS_ROOT 17 | 18 | COPY package.json package.json 19 | RUN yarn install 20 | 21 | RUN rm -f /etc/nginx/sites-enabled/default 22 | RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime 23 | 24 | EXPOSE 3001 25 | CMD ./node_modules/.bin/webpack-dev-server --host 0.0.0.0 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | gem 'highline', :require => 'highline/import' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootpay JS 2 | 3 | ## 현재 버전 API-v1 저장소입니다 4 | * API-v2 (SDK 4.0.0) 버전은 private 저장소로 변경되었고, https://www.npmjs.com/package/@bootpay/client-js 로 변경되었습니다. 5 | * API-v2 (SDK 4.0.0) 버전은 typescript로 작성되었으며, SSR 기반 프레임워크, CDN등 모두 지원합니다. 6 | * 기본 v1 버전은 계속해서 유지보수할 예정이고, 신규 기능은 v2로 지원됩니다. 7 | 8 | ## Change Log 9 | 10 | ### 3.3.6 (Stable) 11 | #### 오래된 development-package-dependency 정리 12 | 13 | ### 3.3.5 14 | #### babel-cli > glob-parent 버전으로 인한 보안 취약점 제거 15 | 16 | ### 3.3.4 17 | #### 오류 발생시 event 전송 버그 수정 ( error event에 Message가 null인 경우 처리 안됨 ) 18 | #### SuperAgent에서 이벤트 전송시 암호화될 때 오류 예외처리 추가 19 | 20 | ### 3.3.3 21 | #### 결제 창 상태 정보 전송시 오류가 나면 관련 에러처리 버그 수정 22 | #### 카카오 정기결제 0원 결제로 설정하면 빌링키만 가져올 수 있도록 개선 23 | 24 | ### 3.3.2 25 | #### 월컴페이먼츠 카드정기결제(디지털) 버그 수정 26 | - 결제방법 digital_card_rebill 추가 27 | 28 | ### 3.3.1 29 | #### 통합결제 1000원 미만 결제 팝업 버그 수정 30 | - method blank 처리 추가, methods 다결제 요청시 validation 추가 31 | 32 | ### 3.3.0 33 | #### popupAsyncRequest 추가 34 | - promise 기반으로 동작하는 함수를 전달할 경우 resolve함수에 request시 필요한 데이터를 전달하면 팝업이 뜨면서 결제창이 생성 35 | - ios safari의 경우 scroll로 인해 iFrame의 터치영역에 버그가 생기는 변수로 인해 해당 문제를 팝업으로 대체 36 | - popupAsyncRequest를 할 경우 조건을 줘서 팝업창을 띄울지 아니면 일반적인 iFrame을 띄울지 선택이 가능 37 | 38 | #### 모바일 custom method 실행 최적화 39 | - 모바일에서 custom 함수 실행시 같은 thread 에서 동시 실행될 경우 무시되는 현상 제거 40 | 41 | #### 팝업으로 통합결제창 진행 42 | - 팝업으로 통합창이 가능하도록 수정 43 | 44 | ### 3.2.6 45 | #### 버그 수정 46 | - 결제 시작 중 금액으로 인한 오류가 발생되었을 때 다시 결제창이 뜨지 않는 버그 수정 47 | 48 | ### 3.2.5 49 | #### 기능 추가 50 | - 가상계좌 팝업으로 띄웠을 경우 iFrame창으로 가상계좌 입금 결과를 볼 수 있도록 기능 추가 51 | 52 | ### 3.2.4 53 | #### 기능 추가 54 | - 팝업으로 결제창 진행시 부모창에서 결제창 닫기 버튼 활성화기능 추가 55 | - 팝업 관련 이벤트 버그 수정 56 | - 단일 결제 Lock 추가 57 | 58 | ### 3.2.3 59 | #### 기능 변경 60 | - 모바일 사파리에서 팝업 결제창을 띄울 경우 동의창이 나왔지만 모든 브라우저 모든 OS에 대해 동의창이 뜨도록 변경 ( window.opener 관련 버그 회피 ) 61 | - quick_popup 기능 추가 ( 결제하기 버튼을 누른 후 팝업이 뜨기 까지 ajax(파폭&크롬)나 300ms이상 지연(사파리) 없이 팝업 차단 없이 바로 결제창을 팝업으로 띄우기 원할때 쓰는 기능 ) 62 | 63 | ### 3.2.2 64 | #### 새로운 기능 65 | - Easy Card 기능 추가 ( 부트페이에서 제공하는 간편결제카드 기능 추가 ) 66 | 67 | ### 3.2.1 68 | #### 버그 수정 69 | - 결제 완료 후 일부 상품명의 조건에서 팝업 창에서 못돌아오는 버그 수정 70 | - UserToken 정보를 보내는 Params 추가 ( 간편결제용 ) 71 | - 결제 팝업 spec이 undefined일 경우 결제 진행 불가 버그 수정 72 | 73 | ### 3.2.0 74 | #### 새로운 기능 75 | - 네이버페이 팝업 유도 버튼이 네이버페이 색상으로 통일 되었습니다. 76 | - 팝업을 띄우기 전 about:blank로 막혔는지 확인하고 안막혔으면 바로 띄우고 막혔으면 팝업창으로 유도하는 버튼을 보여주는 로직이 추가되었습니다. 77 | #### 버그 수정 78 | - 페이앱 네이버페이 IE11에서 결제 완료 처리 안되는 버그 수정되었습니다. 79 | #### 미해결 이슈 80 | - 페이앱 네이버페이는 네이버 보안정책으로 인해 팝업으로 진행됩니다. PC 사파리의 경우 사파리 버그로 인해 팝업 안에서 다른 팝업을 띄울 때 네이버페이쪽에서 팝업 (이미 차단해제가 되었으나) 차단해제를 해달라는 문제가 있습니다. 현재는 네이버페이가 PC 사파리에서는 진행이 되지 않습니다. 81 | 82 | ### 3.1.1 83 | #### 새로운 기능 84 | - delivery_price ( 배송비 ) 를 추가로 받습니다. 85 | #### 변경된 점 86 | - 다날 본인인증 관련 새로운 암호화 방식으로 사용됩니다. ( 기존 3.1.0 이하 사용자분들은 unique값이 달라질 수 있습니다. 유의해주시기 바랍니다. ) 87 | 88 | ### 3.1.0 89 | #### 변경된 점 90 | - Progress 애니메이션 변경 91 | - 결제창 Size Stylesheet 변경 92 | 93 | ### 3.0.8 94 | #### 새로운 기능 95 | - 결제링크 생성 후 결제창을 띄우는 기능 추가 ( 결제 요청 REST API는 공개 예정 ) 96 | 97 | ### 3.0.7 98 | #### 기능변경 99 | - 팝업 결제시 팝업 Instance 체크 interval 3000ms -> 1000ms로 변경 100 | - extra popup option ( 강제적으로 팝업을 띄워서 결제하는 기능 ) 기능 추가 ( 일부 PG만 사용 가능 ) 101 | 102 | ### 3.0.6 103 | #### 버그 수정 104 | - close 함수 실행 시 익명함수가 간헐적으로 모두 실행되는 버그 수정 ( ajax 이후 실행되는 경우 ) 105 | 106 | ### 3.0.5 107 | #### 새로운 기능 108 | - Stage 서버 추가 109 | - JS Version 기록 버그 수정 110 | 111 | ### 3.0.4 112 | #### 기능 변경 113 | - Angular Universal 컴파일시 localStorage 에러 수정 114 | - 안드로이드 (9.0 파이) 팝업 정책 변경으로 우회코드 추가 115 | 116 | ### 3.0.3 117 | #### 기능 변경 118 | - 부트페이 관련 이벤트를 호출시 Binding하도록 변경 119 | 120 | ### 3.0.2 121 | #### 새로운기능 122 | - 네이버페이 주문형 결제 정보 추가 params 123 | - third party API 옵션 기능 추가 124 | 125 | ### 3.0.1 126 | #### 복합기능 127 | - use_order_id: 1 과 같은 1, 0의 값을 직관적으로 true, false도 함께 허용 128 | #### 새로운 기능 129 | - 네이버페이 주문형 요청 추가 130 | - 페이앱 결제 완료 후 창 닫기 옵션 추가 131 | - 결제 완료 후 완료 버튼 노출 추가 132 | 133 | ### 3.0.0 134 | #### 새로운 기능 135 | - 앞으로 새로 제공될 결제 방식(기존 PG 결제 이외의)에 대한 업데이트 이루어졌습니다. 136 | - REST API로 결제 승인이 가능합니다. 좀 더 정밀한 결제 검증이 가능해졌습니다. ( Docs에서 사용법을 업데이트 중 ) 137 | - 일부 PG에서 iFrame이 안되는 문제가 있어서 Mobile Safari를 대응하기 위해 POPUP 결제창 로직이 추가 되었습니다. 일부 PG사의 결제 수단은 POPUP으로 동일한 로직으로 결제가 진행됩니다. 138 | #### 버그 수정내역 139 | - 팝업 결제 일 경우 iOS Safari는 사용자의 개입이 필요한 Direct Interactive 버튼 추가 ( 버튼이 없으면 팝업 차단 되어 있는 경우 팝업이 뜨지 않음 ) 140 | - 결제창 CSS 수정 ( 버튼 색상을 부트페이 메인 컬러로 통일 ) 141 | - iOS 인앱에서 iFrame으로 결제창을 띄울 경우 탭 위치가 올바르지 않는 문제 수정 ( Bug Fixed ) 142 | - Progress Position 모바일 일 경우 약간 위로 수정 143 | - Popup 결제 시작 Trigger 요청시 POST -> GET 방식으로 변경 ( 아이폰 인앱 대응 ) 144 | - 타 Framework에서 postMessage 사용시 json parsing 에러 안나도록 Filter 추가 145 | - iFrame iOS에서 Scroll 버그 수정 ( 일부 PG에서 스크롤이 자연스럽게 내려가지 않는 문제 해결 ) 146 | 147 | ### 2.1.1 148 | - 가맹점에서 order_id를 PK로 PG사로 전송기능 ( KCP만 가능 - 차후 다른 가맹점도 업데이트 예정 - use_order_id: 1로 설정하면 사용 가능 ) 149 | 150 | ### 2.1.0 151 | - IE에서 transactionConfirm 함수가 두번 호출되는 문제가 있습니다. ConfirmLock을 통해 한번만 호출되도록 수정하였습니다. ( Bug Fixed ) 152 | - escrow 결제 여부를 선택하는 부분이 extra에서 보낼 수 있도록 업데이트 되었습니다. ( 기능추가 ) 153 | - 일부 모바일 카드 결제에서 iFrame에서 앱카드 및 ISP가 호출안되는 문제가 있어서 Form 방식 결제를 할 수 있도록 변경이 되었습니다. 요청시 결제 리턴 결과를 받을 return_url params를 보내서 승인 전 데이터를 받을 URL을 설정할 수 있습니다. ( 기능추가 ) 154 | - 통계 데이터를 결제 특정 이벤트에 보내지 않는 버그를 수정하였습니다. ( Bug Fixed ) 155 | 156 | ### 2.0.20 157 | - 결제를 팝업으로 띄워서 요청할 때 팝업과 팝업 Opener의 도메인이 서로 다른 경우 결제 UUID와 접속 UUID를 동기화하는 함수 추가 ( IE에서는 Cross Site postMessage 정책 때문에 해당 기능이 작동하지 않습니다. ) 158 | 159 | ## 부트페이 결제 요청 JS SDK 160 | 코드 한줄로 구현하는 Bootpay JS 모듈입니다. 개발 언어는 coffeescript로 되어 있으며, jQuery 의존성이 있는 1.x.x버전은 Private Git 저장소로 관리중이며, jQuery의존성이 없는 2.x.x는 GitHub에 오픈소스로 개발되었습니다. 161 | 2.x.x는 Webpack으로 컴파일 되며, webpack-dev-server를 통해 테스트 서버로 결제를 테스트 할 수도 있습니다. 162 | 163 | ## NPM URL 164 | NPM으로 다운 받을 수 있는 경로는 다음과 같습니다. 165 | https://www.npmjs.com/package/bootpay-js 166 | 167 | ## 연동 방법 168 | ### 1. CDN으로 Javascript 호출하기 169 | ```html 170 | 171 | ``` 172 | 173 | ### 2. npm으로 설치하기 174 | ```shell 175 | npm install bootpay-js 176 | ``` 177 | 설치 한 후에 178 | ``` javascript 179 | var BootPay = require('bootpay-js'); 180 | ``` 181 | 형태로 사용이 가능합니다. 182 | 183 | ### 3. Webpack Package 사용 184 | ```json 185 | { 186 | "dependencies": { 187 | //... 188 | "bootpay-js": "^3.3.2" 189 | //... 190 | } 191 | } 192 | ``` 193 | 194 | ```coffeescript 195 | import BootPay from 'bootpay-js' 196 | ``` 197 | 198 | ### 4. Require JS 사용 199 | ```html 200 | 208 | ``` 209 | 210 | ## 부트페이로 결제 연동하기 전에 211 | * Bootpay Admin (https://admin.bootpay.co.kr) 로 간 후 먼저 회원가입을 해주세요. 212 | * Bootpay Docs (https://docs.bootpay.co.kr) 로 가셔서 연동전 필요한 준비를 해주세요. 213 | 214 | ## 부트페이 JS 결제창 띄우기 215 | ```javascript 216 | BootPay.request({ 217 | price: 3000, // 결제할 금액 218 | application_id: '(부트페이 관리자에서 Web용 Application ID 입력해주세요.)', 219 | name: '(판매할 아이템이름)', // 아이템 이름, 220 | phone: '(구매자 전화번호 ex) 01000000000)', 221 | order_id: '(이 결제를 식별할 수 있는 고유 주문 번호)', 222 | pg: '(결제창을 띄우려는 PG 회사명 ex) kcp, danal)', 223 | method: '(결제수단 정보 ex) card, phone, vbank, bank)', 224 | show_agree_window: 0, // 결제 동의창 띄우기 여부 1 - 띄움, 0 - 띄우지 않음 225 | items: [ // 결제하려는 모든 아이템 정보 ( 통계 데이터로 쓰이므로 입력해주시면 좋습니다. 입력하지 않아도 결제는 가능합니다.) 226 | { 227 | item_name: '(판매된 아이템 명)', 228 | qty: 1, // 판매한 아이템의 수량 229 | unique: '(아이템을 식별할 수 있는 unique key)', 230 | price: 3000 // 아이템 하나의 단가 231 | } 232 | ], 233 | user_info: { // 구매한 고객정보 ( 통계 혹은 PG사에서 요구하는 고객 정보 ) 234 | email: '(이메일)', 235 | phone: '(고객의 휴대폰 정보)', 236 | username: '구매자성함', 237 | addr: '(고객의 거주지역)' 238 | } 239 | }).error(function (data) { 240 | // 결제가 실패했을 때 호출되는 함수입니다. 241 | var msg = "결제 에러입니다.: " + JSON.stringify(data); 242 | alert(msg); 243 | console.log(data); 244 | }).cancel(function (data) { 245 | // 결제창에서 결제 진행을 하다가 취소버튼을 눌렀을때 호출되는 함수입니다. 246 | var msg = "결제 취소입니다.: " + JSON.stringify(data); 247 | alert(msg); 248 | console.log(data); 249 | }).confirm(function (data) { 250 | // 결제가 진행되고 나서 승인 이전에 호출되는 함수입니다. 251 | // 일부 결제는 이 함수가 호출되지 않을 수 있습니다. ex) 가상계좌 및 카드 수기결제는 호출되지 않습니다. 252 | // 만약 이 함수를 정의하지 않으면 바로 결제 승인이 일어납니다. 253 | if (confirm('결제를 정말 승인할까요?')) { 254 | console.log("do confirm data: " + JSON.stringify(data)); 255 | // 이 함수를 반드시 실행해야 결제가 완전히 끝납니다. 256 | // 부트페이로 서버로 결제를 승인함을 보내는 함수입니다. 257 | this.transactionConfirm(data); 258 | } else { 259 | var msg = "결제가 승인거절되었습니다.: " + JSON.stringify(data); 260 | alert(msg); 261 | console.log(data); 262 | } 263 | }).done(function (data) { 264 | // 결제가 모두 완료되었을 때 호출되는 함수입니다. 265 | alert("결제가 완료되었습니다."); 266 | console.log(data); 267 | }).ready(function (data) { 268 | // 가상계좌 번호가 체번(발급) 되었을 때 호출되는 함수입니다. 269 | console.log(data); 270 | }); 271 | ``` 272 | 273 | ## 각 결제 수단별 결제 진행 순서 274 | ### 카드(card), 휴대폰 소액결제 (phone), 계좌이체 (bank), 간편결제 (카카오 혹은 페이코) 275 | 대부분의 결제는 진행 순서가 동일합니다. 276 | ``` 277 | <결제 요청> -> <결제창이 띄워짐> -> -> < 부트페이 서버에서 결제 승인 > 278 | ``` 279 | ### 가상계좌(vbank) 280 | 가상계좌는 계좌번호 발급 이후 입금 되어야 결제가 완전히 끝나는 특수한 결제 방법입니다. 281 | ``` 282 | <결제 요청> -> <결제창이 띄워짐> -> < ready 함수 호출 및 가상계좌 발급 > -> < 입금 후 부트페이 서버로 결제 정보가 옴 > -> < 부트페이 관리자에서 설정한 FeedbackURL로 가맹점 서버로 결제 데이터 전송 > 283 | ``` 284 | 285 | ## 부트페이 통계 이용하기 286 | 부트페이로 데이터를 보내주시면 1시간 단위, 1일 단위, 1주일 단위, 1달 단위의 데이터를 분석하여 접속 통계 및 결제 통계를 보실 수 있습니다. 287 | 자세한 통계 내용은 https://admin.bootpay.co.kr 로 가셔서 테스트 계정으로 로그인 후 확인하실 수 있습니다. 288 | 289 | ### 특정 페이지 접근시 통계 데이터 전달 290 | 통계를 집계하길 원하는 페이지에 접근했을 때 부트페이로 데이터를 보내 통계를 낼 수 있습니다. 291 | 특정 아이템을 판매하는 페이지에 대한 분석 및 어떤 Referer를 통해 페이지를 진입했는지 여부를 통계 데이터로 뽑아내 줍니다. 292 | ```javascript 293 | // DocumentContentLoaded 이벤트 호출 후에 페이지 정보를 전송하는 것을 추천합니다. 294 | document.addEventListener('DOMContentLoaded', function () { 295 | // 부트페이로 페이지 정보를 전송하는 함수입니다. 296 | // 이 함수를 호출하지 않으면 페이지에 대한 정보를 수집하지 않습니다. 297 | BootPay.startTrace({ 298 | // 현재 페이지에서 판매하는 아이템 정보 ( 상품이 여러개일 수 있으므로 Array로 보내주세요. ) 299 | items: [ 300 | { 301 | item_name: '( 아이템 명 )', 302 | item_img: '( 이미지 URL 경로 )', 303 | unique: '( 아이템 고유 키 )', 304 | cat1: '( 카테고리 상위 1 )', 305 | cat2: '( 카테고리 중위 2 )', 306 | cat3: '( 카테고리 하위 3 )' 307 | }, 308 | { 309 | item_name: '( 아이템 명 )', 310 | item_img: '( 이미지 URL 경로 )', 311 | unique: '( 아이템 고유 키 )', 312 | cat1: '( 카테고리 상위 1 )', 313 | cat2: '( 카테고리 중위 2 )', 314 | cat3: '( 카테고리 하위 3 )' 315 | } 316 | ] 317 | }); 318 | }); 319 | ``` 320 | 321 | ### 로그인한 회원 정보 전달 322 | 어떤 사용자가 로그인을 했는지 로그인 한 후 어떤 아이템을 구매했는지 추적할 수 있습니다. 323 | 그러기 위해서는 로그인 한 정보를 부트페이로 전송하여 데이터 분석을 할 수 있습니다. ( 정보는 통계용으로만 쓰이고 60일 후 모두 폐기 됩니다. ) 324 | ```javascript 325 | // 로그인 한 이후 호출되는 함수 326 | // 로그인 정보를 부트페이로 전송 327 | function LoginAfterCallFunction() { 328 | // 로그인 정보를 보내고 부트페이에서 로그인 세션을 받아 데이터를 추적한다. 329 | BootPay.startLoginSession({ 330 | id: '(회원 아이디)', 331 | username: '(회원 이름)', 332 | birth: '(회원 생년월일)', 333 | phone: '(회원 전화번호)', 334 | email: '(회원 이메일)', 335 | gender: '(회원 성별 1 - 남자, 0 - 여자)', 336 | area: '(회원 거주 지역)' 337 | }); 338 | } 339 | ``` 340 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | ROOT_PATH = `pwd`.to_s.strip 3 | task :service do 4 | todo = [] 5 | file_path = File.join(ROOT_PATH, 'config') 6 | Dir["#{file_path}/*.example.service"].each do |file| 7 | output_file = file.gsub '.example', '' 8 | File.open(output_file, 'wb') do |f| 9 | f.write ERB.new(File.read(file)).result 10 | f.close 11 | end 12 | print "#{output_file} 파일 생성 완료\n" 13 | todo << "ln -s #{output_file} #{output_file.to_s.gsub(file_path, '/lib/systemd/system')}" 14 | todo << "systemctl enable #{output_file.to_s.gsub(file_path, '')}" 15 | print "TODO: 다음 작업을 root 권한으로 반드시 해야합니다.\n#{todo.join("\n")}\n" 16 | end 17 | end 18 | 19 | task :html do 20 | url = { 21 | development: 'https://d-cdn.bootpay.co.kr', 22 | test: 'https://d-cdn.bootpay.co.kr', 23 | production: 'https://cdn.bootpay.co.kr/js' 24 | } 25 | Dir.mkdir('./public') unless Dir.exist?('./public') 26 | version = ENV['BOOTPAY_VERSION'] 27 | Dir.mkdir("./public/#{version}") unless Dir.exist?("./public/#{version}") 28 | url.each do |key, value| 29 | file = "./public/#{version}/#{key.to_s}.html" 30 | ENV['js_url'] = "#{value}/bootpay-#{version}.min.js" 31 | File.open(file, 'wb') do |f| 32 | f.write ERB.new(File.read('./html/template.html')).result 33 | f.close 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$1" ]; then 3 | echo "버전을 입력해주세요."; 4 | exit; 5 | fi 6 | if [ -z "$2" ]; then 7 | MODE=development 8 | else 9 | MODE=$2 10 | fi 11 | echo "JS SDK $1 버전 빌드중입니다."; 12 | node_modules/.bin/webpack --output-filename=bootpay-$1.min.js --output-path=./dist --mode=$MODE 13 | gzip -c dist/bootpay-$1.min.js > dist/bootpay-$1.min.js.gz 14 | echo "JS SDK $1 버전 빌드가 완료되었습니다." 15 | echo "In APP HTML $1 버전 빌드중입니다." 16 | rake html BOOTPAY_VERSION=$1 17 | echo "In APP HTML $1 버전 빌드가 완료되었습니다." -------------------------------------------------------------------------------- /config/bootpay_inapp.example.service: -------------------------------------------------------------------------------- 1 | server { 2 | server_name dev-inapp.bootpay.co.kr;; 3 | rewrite ^ https://$server_name$request_uri? permanent; 4 | } 5 | 6 | server { 7 | listen 443 ssl http2; 8 | server_name dev-inapp.bootpay.co.kr; 9 | access_log /var/log/nginx/dev-inapp.bootpay.co.kr;.access; 10 | error_log /var/log/nginx/dev-inapp.bootpay.co.kr;.error; 11 | proxy_set_header REMOTE_ADDR $remote_addr; 12 | proxy_set_header X-Real-IP $remote_addr; 13 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 14 | proxy_set_header ORIGIN ""; 15 | proxy_set_header Host $http_host; 16 | proxy_set_header X-Forwarded-Host $host; 17 | ssl on; 18 | ssl_certificate /etc/nginx/ssl/bootpay.co.kr/public.pem; 19 | ssl_certificate_key /etc/nginx/ssl/bootpay.co.kr/private.key; 20 | ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2; 21 | ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; 22 | ssl_dhparam /etc/nginx/ssl/dhparam.pem; 23 | ssl_stapling on; 24 | ssl_stapling_verify on; 25 | ssl_trusted_certificate /etc/nginx/ssl/bootpay.co.kr/public.pem; 26 | resolver 8.8.8.8 8.8.4.4; 27 | ssl_prefer_server_ciphers on; 28 | ssl_session_cache shared:SSL_CACHE:4m; 29 | ssl_session_timeout 5m; 30 | 31 | location / { 32 | root ; 33 | } 34 | 35 | client_max_body_size 100M; 36 | error_page 500 502 503 504 /50x.html; 37 | location = /50x.html { 38 | root html; 39 | } 40 | } -------------------------------------------------------------------------------- /config/bootpay_serve_js.example.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=<%= ENV['SERVICE_NAME'] %> Webpack 3 | After=network.target 4 | [Service] 5 | Type=simple 6 | User=<%= `whoami`.to_s.strip %> 7 | WorkingDirectory=<%= `pwd`.to_s.strip %> 8 | ExecStart=<%= `pwd`.to_s.strip %>/node_modules/.bin/webpack-dev-server --mode=development 9 | [Install] 10 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | volumes: 3 | postgres_data: {} 4 | 5 | services: 6 | bootpay_js_dev: 7 | image: docker.bootpay.co.kr/bootpay/bootpay-js:dev 8 | build: 9 | context: . 10 | dockerfile: ./Dockerfile 11 | volumes: 12 | - ./:/var/www/ 13 | ports: 14 | - "10011:3001" 15 | hostname: bootpay-js-v1 16 | container_name: bootpay-js-v1 17 | extra_hosts: 18 | "test-database": 10.0.100.2 -------------------------------------------------------------------------------- /html/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 부트페이 결제 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/bootpay.coffee: -------------------------------------------------------------------------------- 1 | import 'es6-promise/auto' 2 | import ObjectAssign from 'object-assign' 3 | import Analytics from './extend/analytics' 4 | import BootpayEvent from './extend/bootpay_event' 5 | import Common from './extend/common' 6 | import Encrypt from './extend/encrypt' 7 | import Message from './extend/message' 8 | import Notification from './extend/notification' 9 | import Payment from './extend/payment' 10 | import Platform from './extend/platform' 11 | import Storage from './extend/storage' 12 | import Event from './event' 13 | import './style' 14 | 15 | window.BootPay = 16 | VISIT_TIMEOUT: 86400000 # 재 방문 시간에 대한 interval 17 | SK_TIMEOUT: 1800000 # 30분 18 | PAYMENT_LOCK: false 19 | CONFIRM_LOCK: false 20 | applicationId: undefined 21 | version: '3.3.4' 22 | mode: 'production' 23 | backgroundId: 'bootpay-background-window' 24 | windowId: 'bootpay-payment-window' 25 | iframeId: 'bootpay-payment-iframe' 26 | closeId: 'bootpay-progress-button-window' 27 | popupWatchInstance: undefined 28 | popupInstance: undefined 29 | popupData: undefined 30 | ieMinVersion: 9 31 | deviceType: 1 32 | isSetQuickPopup: false 33 | ableDeviceTypes: 34 | JS: 1 35 | ANDROID: 2 36 | IOS: 3 37 | methods: {} 38 | params: {} 39 | option: {} 40 | phoneRegex: /^\d{2,3}\d{3,4}\d{4}$/ 41 | dateFormat: /(\d{4})-(\d{2})-(\d{2})/ 42 | zeroPaymentMethod: ['bankalarm', 'auth', 'card_rebill', 'digital_card_rebill', 'easy_rebill'] 43 | urls: require('../package.json').urls 44 | tk: undefined 45 | localStorage: {} 46 | 47 | initialize: (logLevel = 1) -> 48 | if Element? 49 | Event.startEventBinding() 50 | @setLogLevel logLevel 51 | @setReadyUUID() 52 | @setReadySessionKey() 53 | @bindBootpayCommonEvent() 54 | 55 | ObjectAssign(window.BootPay, Analytics, BootpayEvent, Common, Encrypt, Message, Notification, Payment, Platform, Storage) 56 | window.BootPay.initialize() 57 | 58 | export default window.BootPay -------------------------------------------------------------------------------- /lib/event.coffee: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaces: {} 3 | on: (event, cb, opts) -> 4 | @namespaces = {} unless @namespaces? 5 | @namespaces[event] = cb 6 | options = opts || false 7 | @addEventListener(event.split('.')[0], cb, options) 8 | @ 9 | off: (event) -> 10 | if @namespaces? and @namespaces[event]? 11 | @removeEventListener(event.split('.')[0], @namespaces[event]) 12 | delete @namespaces[event] 13 | @ 14 | startEventBinding: -> 15 | if window? 16 | window.on = @.on 17 | window.off = @.off 18 | if document? 19 | document.on = @.on 20 | document.off = @.off 21 | if Element? 22 | Element::on = @.on 23 | Element::off = @.off 24 | } -------------------------------------------------------------------------------- /lib/extend/analytics.coffee: -------------------------------------------------------------------------------- 1 | import Logger from '../logger' 2 | import AES from 'crypto-js/aes' 3 | import Base64 from 'crypto-js/enc-base64' 4 | import request from 'superagent' 5 | 6 | export default { 7 | # Parent 혹은 Opener에서 데이터를 가져와 통계 데이터를 동기화한다. 8 | setAnalyticsDataByParent: (parent) -> 9 | parent.postMessage(JSON.stringify(action: 'BootpayAnalyticsData'), '*') 10 | 11 | # 기본적인 통계 데이터를 설정한다. 12 | # Android, iPhone에서 기본적으로 사용하는 코드 13 | setAnalyticsData: (data) -> 14 | @setData 'uuid', data.uuid if data.uuid? 15 | @setData 'sk', data.sk if data.sk? 16 | @setData 'sk_time', data.sk_time if data.sk_time? 17 | @setData 'time', data.time if data.time? 18 | 19 | # 로그인한 유저 정보를 가져온다. 20 | getUserData: -> try JSON.parse(@getData('user')) 21 | catch then undefined 22 | # Javascript로 UUID를 생성한다. 23 | generateUUID: -> 24 | 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) -> 25 | r = Math.random() * 16 | 0 26 | v = if c == 'x' then r else r & 0x3 | 0x8 27 | v.toString 16 28 | # 로그인 했을때 데이터를 저장한다. 29 | setUserData: (data) -> 30 | @setData 'user', JSON.stringify(data) 31 | # 세션 키를 발급하는 로직 32 | setReadySessionKey: -> 33 | sessionKeyTime = (new Date()).getTime() 34 | if @getData('last_time')?.length 35 | sessionKeyTime = new Date().getTime() 36 | if @getData('last_time')?.length 37 | lastTime = parseInt(@getData('last_time')) # 마지막으로 접근한 시간을 기록한다. 38 | @setData 'last_time', sessionKeyTime 39 | if isNaN(lastTime) or lastTime + @SK_TIMEOUT < sessionKeyTime # 마지막 접속한 시간에 30분이 지나버린 경우 세션을 초기화한다. 40 | @setData 'sk', "#{@getData('uuid')}-#{sessionKeyTime}" 41 | @setData 'sk_time', sessionKeyTime 42 | @setData 'time', (sessionKeyTime - lastTime) 43 | Logger.debug "시간이 지나 세션 고유 값 정보를 새로 갱신하였습니다. sk: #{@getData('sk')}, time: #{@getData('sk_time')}" 44 | else 45 | Logger.debug "이전 세션을 그대로 이용합니다. sk: #{@getData('sk')}, time: #{@getData('sk_time')}" 46 | else 47 | @setData 'last_time', sessionKeyTime 48 | @setData 'sk', "#{@getData('uuid')}-#{sessionKeyTime}" 49 | @setData 'sk_time', sessionKeyTime 50 | Logger.debug "처음 접속하여 세션 고유 값을 설정하였습니다." 51 | # 로그인 정보를 절차에 따라 초기화한다. 52 | expireUserData: -> 53 | data = @getUserData() 54 | # 데이터가 없거나 접속 한지 하루가 지나면 데이터를 삭제한다. 55 | if data? and (data.time + @VISIT_TIMEOUT < new Date().getTime()) 56 | Logger.info "시간이 지나 로그인 유저 정보를 초기화 하였습니다." 57 | @setData 'user', null 58 | # 통계 시작 59 | startTrace: (data = undefined) -> 60 | @setApplicationId() unless @applicationId?.length 61 | @expireUserData() 62 | @sendCommonData data 63 | # 통계용 데이터를 부트페이로 전송 64 | sendCommonData: (data) -> 65 | url = document.URL 66 | return if !url or (url.search(/g-cdn.bootpay.co.kr/) == -1 and url.search(/bootpay.co.kr/) > -1 and url.search(/app.bootpay.co.kr/) == -1) 67 | user = @getUserData() 68 | items = if data? and data.items?.length then data.items else [ 69 | { 70 | cat1: if data? then data.cat1 else undefined 71 | cat2: if data? then data.cat2 else undefined 72 | cat3: if data? then data.cat3 else undefined 73 | item_img: if data? then data.item_img else undefined 74 | item_name: if data? then data.item_name else undefined 75 | unique: if data? then data.unique else undefined 76 | price: if data? then data.price else undefined 77 | } 78 | ] 79 | requestData = 80 | application_id: @applicationId 81 | uuid: @getData('uuid') 82 | time: @getData('time') 83 | url: if data? and data.url? then data.url else document.URL 84 | referer: if document.referrer?.length and document.referrer.search(new RegExp(window.location.hostname)) == -1 then document.referrer else '' 85 | sk: @getData('sk') 86 | user_id: if user? then user.id else undefined 87 | page_type: if data? then data.type else undefined 88 | items: items 89 | Logger.debug "활동 정보를 서버로 전송합니다. data: #{JSON.stringify(requestData)}" 90 | encryptData = AES.encrypt(JSON.stringify(requestData), requestData.sk) 91 | request.post([@analyticsUrl(), "call?ver=#{@version}"].join('/')).set( 92 | 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' 93 | ).send( 94 | data: encryptData.ciphertext.toString(Base64) 95 | session_key: "#{encryptData.key.toString(Base64)}###{encryptData.iv.toString(Base64)}" 96 | ).then((res) => 97 | Logger.warn "BOOTPAY MESSAGE: #{if res.body? then res.body.message else ''} - Application ID가 제대로 되었는지 확인해주세요." if res.status isnt 200 or res.body.status isnt 200 98 | ).catch((err) => 99 | Logger.warn "BOOTPAY MESSAGE: #{if err.body? then err.body.message else ''} - Application ID가 제대로 되었는지 확인해주세요." 100 | ) 101 | # 로그인 정보를 부트페이 서버로 전송한다. 102 | startLoginSession: (data) -> 103 | try 104 | throw '로그인 데이터를 입력해주세요.' unless data? 105 | throw '로그인 하는 아이디를 입력해주세요.' unless data.id? 106 | catch e 107 | Logger.error e 108 | alert e 109 | throw e 110 | @sendLoginData( 111 | application_id: if data.application_id? then data.application_id else @applicationId 112 | id: data.id 113 | username: data.username 114 | birth: data.birth 115 | phone: data.phone 116 | email: data.email 117 | gender: data.gender 118 | area: if data.area? then String(data.area).match(/서울|인천|대구|광주|부산|울산|경기|강원|충청북도|충북|충청남도|충남|전라북도|전북|전라남도|전남|경상북도|경북|경상남도|경남|제주|세종|대전/) else undefined 119 | ) 120 | # 부트페이 서버로 데이터를 전송한다. 121 | sendLoginData: (data) -> 122 | return if !data? or !document.URL? 123 | Logger.debug "로그인 데이터를 전송합니다. data: #{JSON.stringify(data)}" 124 | data.area = if data.area?.length then data.area[0] else undefined 125 | encryptData = AES.encrypt(JSON.stringify(data), @getData('sk')) 126 | request.post([@analyticsUrl(), "login?ver=#{@version}"].join('/')).set( 127 | 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' 128 | ).send( 129 | data: encryptData.ciphertext.toString(Base64) 130 | session_key: "#{encryptData.key.toString(Base64)}###{encryptData.iv.toString(Base64)}" 131 | ).then((res) => 132 | if res.status isnt 200 or res.body.status isnt 200 133 | Logger.warn "BOOTPAY MESSAGE: #{res.body.message} - Application ID가 제대로 되었는지 확인해주세요." 134 | else 135 | json = res.body.data 136 | @setUserData( 137 | id: json.user_id 138 | time: (new Date()).getTime() 139 | ) 140 | ).catch((err) => 141 | Logger.warn "BOOTPAY MESSAGE: #{err.message} - Application ID가 제대로 되었는지 확인해주세요." 142 | ) 143 | } -------------------------------------------------------------------------------- /lib/extend/bootpay_event.coffee: -------------------------------------------------------------------------------- 1 | import Logger from '../logger' 2 | import request from 'superagent' 3 | 4 | export default { 5 | # 창이 닫혔을 때 이벤트 처리 6 | bindBootpayPaymentEvent: -> 7 | window.off 'message.BootpayGlobalEvent' 8 | window.on('message.BootpayGlobalEvent', (e) => 9 | try 10 | data = {} 11 | data = JSON.parse e.data if e.data? and typeof e.data is 'string' and /Bootpay/.test(e.data) 12 | data.action = data.action.replace(/Child/g, '') if data.action? 13 | catch e 14 | Logger.error "data: #{e.data}, #{e.message} json parse error" 15 | return 16 | switch data.action 17 | # 팝업창으로 결제창 호출 이벤트 18 | # Comment by Gosomi 19 | # Date: 2020-06-30 20 | # @return [undefined] 21 | when 'BootpayPopup' 22 | # iFrame창을 삭제한다. 23 | @popupData = data 24 | @progressMessageHide() 25 | if @isIE() 26 | @startPopupPaymentWindow(data) 27 | else 28 | @showPopupButton() 29 | # 결제창 form submit 방식으로 동작할 때 action 30 | # Comment by Gosomi 31 | # Date: 2020-06-30 32 | # @return [undefined] 33 | when 'BootpayFormSubmit' 34 | for k, v of data.params 35 | input = document.createElement('INPUT') 36 | input.setAttribute('type', 'hidden') 37 | input.setAttribute('name', k) 38 | input.value = v 39 | document.__BOOTPAY_TOP_FORM__.appendChild(input) 40 | document.__BOOTPAY_TOP_FORM__.action = data.url 41 | document.__BOOTPAY_TOP_FORM__.acceptCharset = data.charset 42 | document.__BOOTPAY_TOP_FORM__.submit() 43 | # 사용자가 결제창을 취소했을 때 이벤트 44 | # Comment by Gosomi 45 | # Date: 2020-06-30 46 | # @return [undefined] 47 | when 'BootpayCancel' 48 | @progressMessageShow '취소중입니다.' 49 | try 50 | @clearEnvironment() 51 | @cancelMethodCall(data) if @methods.cancel? 52 | catch e 53 | @sendPaymentStepData( 54 | step: 'cancel' 55 | status: -1 56 | e: e 57 | ) 58 | throw e 59 | @sendPaymentStepData( 60 | step: 'cancel' 61 | status: 1 62 | ) 63 | @removePaymentWindow() 64 | # 가상계좌 입금 대기 상태 ( 계좌 발급이 되었을 때 호출 ) 65 | # Comment by Gosomi 66 | # Date: 2020-06-30 67 | # @return [undefined] 68 | when 'BootpayBankReady' 69 | try 70 | @progressMessageHide() 71 | @clearEnvironment(if @popupInstance? then 0 else 1) 72 | @readyMethodCall(data) 73 | catch e 74 | @sendPaymentStepData( 75 | step: 'ready' 76 | status: -1 77 | e: e 78 | ) 79 | throw e 80 | @sendPaymentStepData( 81 | step: 'ready' 82 | status: 1 83 | ) 84 | # 결제 승인 전 action 85 | # Comment by Gosomi 86 | # Date: 2020-06-30 87 | # @return [undefined] 88 | when 'BootpayConfirm' 89 | @progressMessageShow '승인중입니다.' 90 | try 91 | @clearEnvironment() 92 | if !@methods.confirm? 93 | @transactionConfirm data 94 | else 95 | @confirmMethodCall(data) 96 | catch e 97 | @sendPaymentStepData( 98 | step: 'confirm' 99 | status: -1 100 | e: e 101 | ) 102 | throw e 103 | @sendPaymentStepData( 104 | step: 'confirm' 105 | status: 1 106 | ) 107 | # 결제창 resize 이벤트 108 | # Comment by Gosomi 109 | # Date: 2020-06-30 110 | # @return [undefined] 111 | when 'BootpayResize' 112 | iframeSelector = document.getElementById(@iframeId) 113 | backgroundSelector = document.getElementById(@backgroundId) 114 | closeSelector = document.getElementById(@closeId) 115 | if data.reset 116 | iframeSelector.removeAttribute 'style' 117 | backgroundSelector.removeAttribute 'style' 118 | closeSelector.removeAttribute 'style' 119 | iframeSelector.setAttribute('scrolling', undefined) 120 | else 121 | iframeSelector.style.setProperty('max-width', data.width) 122 | iframeSelector.style.setProperty('width', '100%') 123 | iframeSelector.style.setProperty('height', data.height) 124 | iframeSelector.style.setProperty('max-height', data.maxHeight) 125 | iframeSelector.style.setProperty('background-color', data.backgroundColor) if data.backgroundColor? 126 | backgroundSelector.style.setProperty('background-color', 'transparent') if data.transparentMode is 'true' 127 | closeSelector.style.setProperty('display', 'block') if data.showCloseWindow is 'true' 128 | # ie 9이하에서는 overflow 속성을 인식하지 못한다. 129 | iframeSelector.style.overflow = data.overflow 130 | iframeSelector.setAttribute 'scrolling', data.scrolling if data.scrolling? 131 | # 결제 진행시 오류가 났을 때 호출 되는 action 132 | # Comment by Gosomi 133 | # Date: 2020-06-30 134 | # @return [undefined] 135 | when 'BootpayError' 136 | try 137 | @clearEnvironment() 138 | @errorMethodCall(data) 139 | catch e 140 | @sendPaymentStepData( 141 | step: 'error' 142 | status: -1 143 | msg: e 144 | ) 145 | throw e 146 | @sendPaymentStepData( 147 | step: 'error' 148 | status: 1 149 | ) 150 | @removePaymentWindow() 151 | # 결제 완료시 호출되는 action 152 | # Comment by Gosomi 153 | # Date: 2020-06-30 154 | # @return [undefined] 155 | when 'BootpayDone' 156 | @progressMessageHide() 157 | try 158 | @clearEnvironment(data.popup_close) 159 | @doneMethodCall(data) 160 | catch e 161 | @sendPaymentStepData( 162 | step: 'done' 163 | status: -1 164 | e: e 165 | ) 166 | throw e 167 | @sendPaymentStepData( 168 | step: 'done' 169 | status: 1 170 | ) 171 | isClose = if data.is_done_close? then data.is_done_close else true 172 | @removePaymentWindow() if isClose 173 | # 사용자 혹은 PG에서 창이 닫히는 action 174 | # Comment by Gosomi 175 | # Date: 2020-06-30 176 | # @return [undefined] 177 | when 'BootpayClose' 178 | @clearEnvironment() 179 | @progressMessageHide() 180 | @removePaymentWindow() 181 | # iFrame 결제창을 app에서 notify 받아 보여준다 182 | # Comment by Gosomi 183 | # Date: 2020-06-30 184 | # @return [undefined] 185 | when 'BootpayShowPaymentWindow' 186 | # iframe 창의 결제창을 보여준다 187 | document.getElementById(@iframeId).style.removeProperty('display') 188 | document.getElementById(@iframeId).style.setProperty('height', '100%') 189 | ) 190 | bindBootpayCommonEvent: -> 191 | window.off 'message.BootpayCommonEvent' 192 | window.on('message.BootpayCommonEvent', (e) => 193 | try 194 | data = {} 195 | data = JSON.parse e.data if e.data? and typeof e.data is 'string' and /Bootpay/.test(e.data) 196 | catch e 197 | Logger.debug "data: #{e.data}, #{e.message} json parse error" 198 | return 199 | switch data.action 200 | when 'BootpayAnalyticsData' 201 | e.source.postMessage(JSON.stringify( 202 | action: 'BootpayAnalyticsReceived' 203 | uuid: @getData('uuid') 204 | sk: @getData('sk') 205 | sk_time: @getData('sk_time') 206 | time: @getData('time') 207 | user: @getData('user') 208 | ), '*') 209 | when 'BootpayAnalyticsReceived' 210 | Logger.debug "receive analytics data: #{JSON.stringify(data)}" 211 | @setAnalyticsData(data) 212 | ) 213 | # 비밀번호 인증 관련 event를 받는다 214 | # Comment by Gosomi 215 | # Date: 2020-10-10 216 | # @return [Hash] 217 | bindEasyEvent: -> 218 | window.off 'message.BootpayEasyEvent' 219 | window.on('message.BootpayEasyEvent', (e) => 220 | try 221 | data = {} 222 | data = JSON.parse e.data if e.data? and typeof e.data is 'string' and /Bootpay/.test(e.data) 223 | data.action = data.action.replace(/Child/g, '') if data.action? 224 | catch e 225 | Logger.error "data: #{e.data}, #{e.message} json parse error" 226 | return 227 | switch data.action 228 | when 'BootpayEasyError' 229 | @methods.easyError(data) if @methods.easyError? 230 | @removeVerifyWindow() 231 | when 'BootpayEasySuccess' 232 | @methods.easySuccess(data) if @methods.easySuccess? 233 | @removeVerifyWindow() 234 | when 'BootpayEasyCancel' 235 | @methods.easyCancel(data) if @methods.easyCancel? 236 | @removeVerifyWindow() 237 | ) 238 | # 강제로 창을 닫는다 239 | # Comment by Gosomi 240 | # Date: 2020-02-13 241 | # @return [undefined] 242 | forceClose: (message = undefined) -> 243 | @cancelMethodCall( 244 | action: 'BootpayCancel', 245 | message: if message? then message else '사용자에 의한 취소' 246 | ) 247 | @removePaymentWindow() 248 | 249 | timeIntervalByPlatform: -> 250 | if @isMobile() then 300 else 0 251 | 252 | # 결제창을 삭제한다. 253 | removePaymentWindow: (callClose = true) -> 254 | # Payment Lock을 해제한다 255 | @setPaymentLock(false) 256 | @progressMessageHide() 257 | document.body.style.removeProperty('bootpay-modal-open') 258 | try document.body.classList.remove('bootpay-open') 259 | catch then '' 260 | document.getElementById(@windowId).outerHTML = '' if document.getElementById(@windowId)? 261 | try 262 | @closeMethodCall() if callClose 263 | catch e 264 | @sendPaymentStepData( 265 | step: 'close' 266 | status: -1 267 | e: e 268 | ) 269 | throw e 270 | @sendPaymentStepData( 271 | step: 'close' 272 | status: 1 273 | ) 274 | @tk = undefined 275 | 276 | # 비밀번호 창을 닫는다 277 | removeVerifyWindow: -> 278 | @progressMessageHide() 279 | document.body.style.removeProperty('bootpay-modal-open') 280 | try document.body.classList.remove('bootpay-open') 281 | catch then '' 282 | document.getElementById(@windowId).outerHTML = '' if document.getElementById(@windowId)? 283 | 284 | closePopupWithPaymentWindow: -> 285 | if confirm "결제창을 닫게 되면 현재 진행중인 결제가 취소됩니다. 정말로 닫을까요?" 286 | @clearEnvironment(true) 287 | @removePaymentWindow() 288 | 289 | cancel: (method) -> 290 | @methods.cancel = method 291 | @ 292 | confirm: (method) -> 293 | @methods.confirm = method 294 | @ 295 | ready: (method) -> 296 | @methods.ready = method 297 | @ 298 | error: (method) -> 299 | @methods.error = method 300 | @ 301 | done: (method) -> 302 | @methods.done = method 303 | @ 304 | close: (method) -> 305 | @methods.close = method 306 | @ 307 | 308 | easyCancel: (method) -> 309 | @methods.easyCancel = method 310 | @ 311 | 312 | easySuccess: (method) -> 313 | @methods.easySuccess = method 314 | @ 315 | 316 | easyError: (method) -> 317 | @methods.easyError = method 318 | @ 319 | 320 | setConfirmLock: (lock) -> 321 | @CONFIRM_LOCK = lock 322 | 323 | setPaymentLock: (lock) -> 324 | @PAYMENT_LOCK = lock 325 | 326 | isPaymentLock: -> 327 | @PAYMENT_LOCK 328 | 329 | isConfirmLock: -> 330 | @CONFIRM_LOCK 331 | 332 | # 결제 실행 단계를 로그로 보낸다. 333 | sendPaymentStepData: (data) -> 334 | return if !@tk? or !@applicationId? # Transaction key가 없다면 실행할 필요가 없다. 335 | data.version = @version 336 | data.tk = @tk 337 | data.application_id = @applicationId 338 | if data.e? 339 | data.msg = try data.e.message 340 | catch then '' 341 | data.trace = try data.e.stack 342 | catch then undefined 343 | request.post([@analyticsUrl(), "event"].join('/')).set( 344 | 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' 345 | ).send( 346 | @encryptParams(data) 347 | ).then((res) => 348 | Logger.debug "BOOTPAY MESSAGE: 결제 이벤트 데이터 정보 전송" 349 | ).catch((err) => 350 | Logger.error "BOOTPAY MESSAGE: 결제 이벤트 데이터 정보 전송실패 #{JSON.stringify(err)}" 351 | ) 352 | 353 | # 팝업 watcher를 삭제한다 354 | # 창이 닫힌다면 팝업창도 강제로 닫는다 355 | clearEnvironment: (isClose = 1) -> 356 | clearInterval(@popupWatchInstance) if @popupWatchInstance? 357 | isClose = if isClose? then isClose else 1 358 | if @popupInstance? and isClose 359 | @popupInstance.close() 360 | @popupInstance = undefined 361 | 362 | # 팝업 창이 시작될 때 각 이벤트를 binding하고 363 | # 팝업창을 띄우고나서 팝업이 닫히는지 매번확인한다 364 | startPopupPaymentWindow: (data) -> 365 | if @isMobileSafari 366 | window.off('pagehide.bootpayUnload') 367 | window.on('pagehide.bootpayUnload', => 368 | @popupInstance.close() if @popupInstance? 369 | ) 370 | else 371 | window.off('beforeunload.bootpayUnload') 372 | window.on('beforeunload.bootpayUnload', => 373 | @popupInstance.close() if @popupInstance? 374 | ) 375 | 376 | document.getElementById(@iframeId).style.display = 'none'; 377 | @clearEnvironment() 378 | @hideProgressButton() 379 | @progressMessageShow('팝업창을 닫으면 종료됩니다.', true) 380 | query = [] 381 | for k, v of data.params 382 | query.push("#{k}=#{v}") if ['su', 'pa_id'].indexOf(k) > -1 383 | setTimeout(=> 384 | @popupInstance.close() if @popupInstance? 385 | # 플랫폼에서 설정해야할 정보를 가져온다 386 | platform = try data.params.pe[@platformSymbol()] 387 | catch then {} 388 | left = try if window.screen.width < platform.width then 0 else (window.screen.width - platform.width) / 2 389 | catch then '100' 390 | top = try if window.screen.height < platform.height then 0 else (window.screen.height - platform.height) / 2 391 | catch then '100' 392 | spec = if platform? and platform.width? and platform.width > 0 then "width=#{platform.width},height=#{platform.height},top=#{top},left=#{left},scrollbars=yes,toolbar=no, location=no, directories=no, status=no, menubar=no" else '' 393 | @popupInstance = window.open("#{data.submit_url}?#{query.join('&')}", "bootpay_inner_popup_#{(new Date).getTime()}", spec) 394 | return window.postMessage( 395 | JSON.stringify( 396 | action: 'BootpayError' 397 | message: '브라우저의 팝업이 차단되어 결제가 중단되었습니다. 브라우저 팝업 허용을 해주세요.' 398 | ) 399 | , '*') unless @popupInstance? 400 | # 팝업 창이 닫혔는지 계속해서 찾는다 401 | @popupWatchInstance = setInterval(=> 402 | if @popupInstance.closed # 창을 닫은 경우 403 | clearInterval(@popupWatchInstance) if @popupWatchInstance? 404 | if @isMobileSafari then window.off('pagehide.bootpayUnload') else window.off('beforeunload.bootpayUnload') 405 | # IE 인 경우에 팝업이 뜨면 결제가 완료되었는지 데이터를 확인해본다 406 | if @isIE() 407 | request.put([@restUrl(), "confirm", "#{data.params.su}.json"].join('/')).set( 408 | 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' 409 | ).send( 410 | @encryptParams( 411 | application_id: @applicationId 412 | tk: @tk 413 | ) 414 | ).then((res) => 415 | if res.body? and res.body.code is 0 416 | setTimeout(=> 417 | window.postMessage( 418 | JSON.stringify( 419 | res.body.data 420 | ) 421 | , '*') 422 | , 300) 423 | else 424 | window.postMessage( 425 | JSON.stringify( 426 | action: 'BootpayCancel' 427 | message: '팝업창을 닫았습니다.' 428 | ) 429 | , '*') 430 | ).catch((err) => 431 | window.postMessage( 432 | JSON.stringify( 433 | action: 'BootpayCancel' 434 | message: "팝업창을 닫았습니다." 435 | ) 436 | , '*') 437 | ) 438 | else 439 | window.postMessage( 440 | JSON.stringify( 441 | action: 'BootpayCancel' 442 | message: '팝업창을 닫았습니다.' 443 | ) 444 | , '*') 445 | , 300) 446 | , 100) 447 | 448 | showPopupEventProgress: -> 449 | if @isMobileSafari 450 | window.off('pagehide.bootpayUnload') 451 | window.on('pagehide.bootpayUnload', => 452 | @popupInstance.close() if @popupInstance? 453 | ) 454 | else 455 | window.off('beforeunload.bootpayUnload') 456 | window.on('beforeunload.bootpayUnload', => 457 | @popupInstance.close() if @popupInstance? 458 | ) 459 | @progressMessageShow('팝업창을 닫으면 종료됩니다.', true) 460 | @popupWatchInstance = setInterval(=> 461 | if @popupInstance.closed # 창을 닫은 경우 462 | clearInterval(@popupWatchInstance) if @popupWatchInstance? 463 | if @isMobileSafari then window.off('pagehide.bootpayUnload') else window.off('beforeunload.bootpayUnload') 464 | # IE 인 경우에 팝업이 뜨면 결제가 완료되었는지 데이터를 확인해본다 465 | if @isIE() and @params.tk? 466 | request.put([@restUrl(), "confirm", "#{@tk}.json"].join('/')).set( 467 | 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' 468 | ).send( 469 | @encryptParams( 470 | application_id: @applicationId 471 | method: 'transaction_key' 472 | tk: @tk 473 | ) 474 | ).then((res) => 475 | if res.body? and res.body.code is 0 476 | setTimeout(=> 477 | window.postMessage( 478 | JSON.stringify( 479 | res.body.data 480 | ) 481 | , '*') 482 | , 300) 483 | else 484 | window.postMessage( 485 | JSON.stringify( 486 | action: 'BootpayCancel' 487 | message: '팝업창을 닫았습니다.' 488 | ) 489 | , '*') 490 | ).catch((err) => 491 | window.postMessage( 492 | JSON.stringify( 493 | action: 'BootpayCancel' 494 | message: "팝업창을 닫았습니다." 495 | ) 496 | , '*') 497 | ) 498 | else 499 | window.postMessage( 500 | JSON.stringify( 501 | action: 'BootpayCancel' 502 | message: '팝업창을 닫았습니다.' 503 | ) 504 | , '*') 505 | , 300) 506 | 507 | 508 | showPopupButton: -> 509 | alias = try @popupData.params.payment.pm_alias 510 | catch then '' 511 | buttonObject = document.getElementById("__bootpay-close-button") 512 | buttonObject.classList.remove('naverpay-btn') 513 | # 네이버페이인 경우 네이버페이 색상으로 편집 514 | if alias is 'npay' 515 | document.getElementById("__bootpay_close_button_title").innerText = '네이버페이로 결제를 시작합니다' 516 | buttonObject.innerText = '네이버페이로 결제하기' 517 | buttonObject.classList.add('naverpay-btn') 518 | @showProgressButton() 519 | } -------------------------------------------------------------------------------- /lib/extend/common.coffee: -------------------------------------------------------------------------------- 1 | import Logger from '../logger' 2 | export default { 3 | # RestURL 정보 4 | restUrl: -> 5 | @urls.restUrl[@mode] 6 | # 클라이언트 URL 정보 7 | clientUrl: -> 8 | @urls.clientUrl[@mode] 9 | # Analytics URL 정보 10 | analyticsUrl: -> 11 | @urls.analyticsUrl[@mode] 12 | # meta tag에서 application id를 찾는다. 13 | setApplicationId: (applicationId = undefined) -> 14 | if applicationId? 15 | @applicationId = applicationId 16 | else 17 | metaTag = document.querySelector('meta[name="bootpay-application-id"]') 18 | if metaTag? 19 | @applicationId = metaTag.getAttribute 'content' 20 | else 21 | return alert ' 다음과 같이 안에 넣어주세요' 22 | # 로그 레벨을 설정한다. 23 | setLogLevel: (logLevel = 1) -> Logger.setLogLevel logLevel 24 | # 사용할 환경 mode를 설정한다 25 | setMode: (mode) -> 26 | @mode = mode 27 | 28 | # device Type을 설정한다. 없을 경우 false를 리턴, 있는 경우 true를 리턴 29 | setDevice: (deviceType) -> 30 | @deviceType = @ableDeviceTypes[deviceType] if @ableDeviceTypes[deviceType]? 31 | @ableDeviceTypes[deviceType]? 32 | 33 | cancelMethodCall: (data = {}) -> 34 | setTimeout(=> 35 | @methods.cancel.call @, data if @methods.cancel? 36 | , @timeIntervalByPlatform()) 37 | 38 | errorMethodCall: (data = {}) -> 39 | setTimeout(=> 40 | @methods.error.call @, data if @methods.error? 41 | , @timeIntervalByPlatform()) 42 | 43 | confirmMethodCall: (data = {}) -> 44 | setTimeout(=> 45 | @methods.confirm.call @, data if @methods.confirm? 46 | , @timeIntervalByPlatform()) 47 | 48 | readyMethodCall: (data = {}) -> 49 | setTimeout(=> 50 | @methods.ready.call @, data if @methods.ready? 51 | , @timeIntervalByPlatform()) 52 | 53 | closeMethodCall: (data = {}) -> 54 | setTimeout(=> 55 | @methods.close.call @, data if @methods.close? 56 | , @timeIntervalByPlatform()) 57 | 58 | doneMethodCall: (data = {}) -> 59 | setTimeout(=> 60 | @methods.done.call @, data if @methods.done? 61 | , @timeIntervalByPlatform()) 62 | 63 | 64 | } -------------------------------------------------------------------------------- /lib/extend/encrypt.coffee: -------------------------------------------------------------------------------- 1 | import AES from 'crypto-js/aes' 2 | import Base64 from 'crypto-js/enc-base64' 3 | 4 | export default { 5 | encryptParams: (data) -> 6 | encryptData = AES.encrypt(JSON.stringify(data), @getData('sk')) 7 | { 8 | data: encryptData.ciphertext.toString(Base64), 9 | session_key: "#{encryptData.key.toString(Base64)}###{encryptData.iv.toString(Base64)}" 10 | } 11 | } -------------------------------------------------------------------------------- /lib/extend/message.coffee: -------------------------------------------------------------------------------- 1 | export default { 2 | progressMessageHide: -> 3 | try 4 | pms = document.getElementById('bootpay-progress-message') 5 | pms.style.setProperty('display', 'none') 6 | document.getElementById('progress-message-text').innerText = '' 7 | document.getElementById(@iframeId).removeEventListener('load', @progressMessageHide) 8 | catch then return 9 | 10 | progressMessageShow: (msg, closeButton = false) -> 11 | pms = document.getElementById('bootpay-progress-message') 12 | pms.style.setProperty('display', 'block') 13 | document.getElementById('progress-message-text').innerText = msg 14 | btn = document.getElementById('__bootpay-popup-close-button__') 15 | btnStyle = if closeButton then 'block' else 'none' 16 | btn.style.setProperty('display', btnStyle) 17 | 18 | showProgressButton: -> 19 | clb = document.getElementById(@closeId) 20 | clb.style.setProperty('display', 'block') 21 | 22 | hideProgressButton: -> 23 | clb = document.getElementById(@closeId) 24 | clb.style.setProperty('display', 'none') 25 | } -------------------------------------------------------------------------------- /lib/extend/notification.coffee: -------------------------------------------------------------------------------- 1 | import Logger from '../logger' 2 | import request from 'superagent' 3 | 4 | export default { 5 | # 결제 정보를 서버로 전송 6 | notify: (data, success = undefined, error = undefined, timeout = 3000) -> 7 | @removePaymentWindow(false) 8 | user = @getUserData() 9 | @applicationId = if data.application_id? then data.application_id else @applicationId 10 | @params = {} 11 | @params.device_type = @deviceType 12 | @params.method = data.method if data.method? 13 | @params.application_id = @applicationId 14 | @params.name = data.name 15 | @params.user_info = data.user_info 16 | @params.redirect_url = if data.redirect_url? then data.redirect_url else '' 17 | @params.return_url = if data.return_url? then data.return_url else '' 18 | @params.phone = if data.phone?.length then data.phone.replace(/-/g, '') else '' 19 | @params.uuid = if data.uuid?.length then data.uuid else @getData('uuid') 20 | @params.order_id = if data.order_id? then String(data.order_id) else undefined 21 | @params.order_info = if data.order_info? then data.order_info else {} # 네이버페이 order 정보 22 | @params.sk = @getData('sk') 23 | @params.time = @getData('time') 24 | @params.price = data.price 25 | @params.delivery_price = data.delivery_price 26 | @params.format = @option.format if data.format? 27 | @params.params = data.params 28 | @params.user_id = if user? then user.id else undefined 29 | @params.bank_account = if data.bank_account? then data.bank_account else undefined 30 | @params.bank_name = if data.bank_name? then data.bank_name else undefined 31 | @params.order_unique = if data.order_unique? then data.order_unique else 0 32 | @integrityItemData() if @params.items?.length 33 | @params.items = data.items 34 | @integrityParams() if !@params.method? or !@params.method isnt 'auth' 35 | request.post([@restUrl(), "notify?ver=#{@version}&format=json"].join('/')).set( 36 | 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' 37 | ).timeout( 38 | response: timeout 39 | deadline: timeout 40 | ).send( 41 | @encryptParams(@params) 42 | ).then((res) => 43 | if res.status isnt 200 or res.body.status isnt 200 44 | Logger.error "BOOTPAY MESSAGE: #{res.body.message} - Application ID가 제대로 되었는지 확인해주세요." 45 | error.apply @, ["BOOTPAY MESSAGE: #{res.body.message} - Application ID가 제대로 되었는지 확인해주세요.", res.body] if error? 46 | else 47 | success.apply @, [res.body.data] if success? 48 | ).catch((err) => 49 | error.apply @, ["서버 오류로 인해 결제가 되지 않았습니다. #{err.message}"] if error? 50 | ) 51 | } -------------------------------------------------------------------------------- /lib/extend/payment.coffee: -------------------------------------------------------------------------------- 1 | import Logger from '../logger' 2 | import request from 'superagent' 3 | 4 | export default { 5 | # 결제 정보를 보내 부트페이에서 결제 정보를 띄울 수 있게 한다. 6 | request: (data, lazy = false) -> 7 | return if @isPaymentLock() 8 | @removePaymentWindow(false) 9 | @setPaymentLock(true) 10 | @bindBootpayPaymentEvent() 11 | @setConfirmLock(false) 12 | try 13 | user = @getUserData() 14 | # 결제 효청시 application_id를 입력하면 덮어 씌운다. ( 결제 이후 버그를 줄이기 위한 노력 ) 15 | @applicationId = data.application_id if data.application_id? 16 | @tk = "#{@generateUUID()}-#{(new Date).getTime()}" 17 | @params = 18 | application_id: @applicationId 19 | show_agree_window: if data.show_agree_window? then data.show_agree_window else 0 20 | device_type: @deviceType 21 | method: data.method if data.method? 22 | methods: data.methods if data.methods? 23 | user_token: data.user_token if data.user_token? 24 | pg: data.pg if data.pg? 25 | name: data.name 26 | items: data.items if data.items?.length 27 | redirect_url: if data.redirect_url? then data.redirect_url else '' 28 | return_url: if data.return_url? then data.return_url else '' 29 | phone: if data.phone?.length then data.phone.replace(/-/g, '') else '' 30 | uuid: if data.uuid?.length then data.uuid else @getData('uuid') 31 | order_id: if data.order_id? then String(data.order_id) else '' 32 | use_order_id: if data.use_order_id? then data.use_order_id else 0 33 | user_info: if data.user_info? then data.user_info else undefined 34 | order_info: if data.order_info? then data.order_info else {} # 네이버페이 order 정보 35 | sk: @getData('sk') 36 | time: @getData('time') 37 | price: data.price 38 | tax_free: if data.tax_free? then data.tax_free else 0 39 | format: if data.format? then data.format else 'json' 40 | params: if data.params? then data.params else undefined 41 | user_id: if user? then user.id else undefined 42 | path_url: document.URL 43 | extra: if data.extra? then data.extra else undefined 44 | account_expire_at: if data.account_expire_at? then data.account_expire_at else undefined 45 | tk: @tk 46 | # 각 함수 호출 callback을 초기화한다. 47 | # async의 경우엔 초기화하지 않는다 48 | @methods = {} unless lazy 49 | @extraValueAppend() 50 | # 아이템 정보의 Validation 51 | @integrityItemData() if @params.items?.length 52 | # 결제 정보 데이터의 Validation 53 | @integrityParams() 54 | # True, False의 데이터를 1, 0으로 변경하는 작업을 한다 55 | @generateTrueFalseParams() 56 | # 데이터를 AES로 암호화한다. 57 | encryptData = @encryptParams(@params) 58 | html = """ 59 |
60 |
61 | 62 | 63 |
64 |
65 |
66 |
67 |
68 |
#{@iframeHtml('')}
69 |
70 | """ 71 | document.body.insertAdjacentHTML 'beforeend', html 72 | try document.body.classList.add('bootpay-open') 73 | catch then '' 74 | @start() 75 | catch e 76 | @sendPaymentStepData( 77 | step: 'start' 78 | status: -1 79 | e: e 80 | ) 81 | @setPaymentLock(false) 82 | throw e 83 | @sendPaymentStepData( 84 | step: 'start' 85 | status: 1 86 | ) 87 | @ 88 | 89 | startPaymentByUrl: (url, tk = undefined) -> 90 | try 91 | @bindBootpayPaymentEvent() 92 | @removePaymentWindow(false) 93 | @setConfirmLock(false) 94 | @tk = if tk?.length then tk else "#{@generateUUID()}-#{(new Date).getTime()}" 95 | html = """ 96 |
97 |
98 | 99 |
100 |
101 |
102 |
103 |
104 |
#{@iframeHtml('')}
105 |
106 | """ 107 | document.body.insertAdjacentHTML 'beforeend', html 108 | try document.body.classList.add('bootpay-open') 109 | catch then '' 110 | @start() 111 | catch e 112 | @sendPaymentStepData( 113 | step: 'start' 114 | status: -1 115 | e: e 116 | ) 117 | throw e 118 | @sendPaymentStepData( 119 | step: 'start' 120 | status: 1 121 | ) 122 | @ 123 | 124 | # 결제 요청 정보 Validation 125 | integrityParams: -> 126 | price = parseFloat @params.price 127 | try 128 | throw '결제할 금액을 설정해주세요. ( 100원 이상, 본인인증/정기 결제요청의 경우엔 0원을 입력해주세요. ) [ params: price ]' if (isNaN(price) or price < 100) and (@zeroPaymentMethod.indexOf(@params.method) is -1 or (not @params.method?.length or not @params.pg?.length)) 129 | throw '판매할 상품명을 입력해주세요. [ params: name ]' unless @params.name?.length 130 | throw '익스플로러 8이하 버전에서는 결제가 불가능합니다.' if @blockIEVersion() 131 | throw '휴대폰 번호의 자리수와 형식이 맞지 않습니다. [ params : phone ]' if @params.phone?.length and !@phoneRegex.test(@params.phone) 132 | throw '판매하려는 제품 order_id를 지정해주셔야합니다. 다른 결제 정보와 겹치지 않은 유니크한 값으로 정해서 보내주시기 바랍니다. [ params: order_id ]' unless @params.order_id?.length 133 | throw '가상계좌 입금 만료일 포멧이 잘못되었습니다. yyyy-mm-dd로 입력해주세요. [ params: account_expire_at ]' if @params.account_expire_at?.length and !@dateFormat.test(@params.account_expire_at) and @params.method is 'vbank' 134 | throw '선택 제한 결제 수단 설정은 배열 형태로 보내주셔야 합니다. [ params: methods, ex) ["card", "phone"] ]' if @params.methods? and !Array.isArray(@params.methods) 135 | catch e 136 | alert e 137 | Logger.error e 138 | throw e 139 | # 아이템 정보 Validation 140 | integrityItemData: -> 141 | try 142 | throw '아이템 정보가 배열 형태가 아닙니다.' unless Array.isArray(@params.items) 143 | @params.items.forEach (item, index) -> 144 | throw "통계에 필요한 아이템 이름이 없습니다. [key: item_name, index: #{index}] " unless item.item_name?.length 145 | throw "통계에 필요한 상품 판매 개수가 없습니다. [key: qty, index: #{index}]" unless item.qty? 146 | throw "상품 판매 개수를 숫자로 입력해주세요. [key: qty, index: #{index}]" if isNaN(parseInt(item.qty)) 147 | throw "통계를 위한 상품의 고유값을 넣어주세요. [key: unique, index: #{index}]" unless item.unique?.length 148 | throw "통계를 위해 상품의 개별 금액을 넣어주세요. [key: price, index: #{index}]" unless item.price? 149 | throw "상품금액은 숫자로만 가능합니다. [key: price, index: #{index}]" if isNaN(parseInt(item.price)) 150 | catch e 151 | alert e 152 | Logger.error e 153 | throw e 154 | # True, False -> 1, 0 으로 Generate 한다 155 | generateTrueFalseParams: -> 156 | for index of @params 157 | @params[index] = 1 if @params[index] is true 158 | @params[index] = 0 if @params[index] is false 159 | 160 | if @params.extra? 161 | for index of @params.extra 162 | @params.extra[index] = 1 if @params.extra[index] is true 163 | @params.extra[index] = 0 if @params.extra[index] is false 164 | 165 | if @params.third_party? 166 | for index of @params.third_party 167 | if @params.third_party[index]? and (typeof @params.third_party[index] is 'object') 168 | for key of @params.third_party[index] 169 | @params.third_party[index][key] = 1 if @params.third_party[index][key] is true 170 | @params.third_party[index][key] = 0 if @params.third_party[index][key] is false 171 | else 172 | @params.third_party[index] = 1 if @params.third_party[index] is true 173 | @params.third_party[index] = 0 if @params.third_party[index] is false 174 | 175 | # Extra value를 조건에 맞게 추가 176 | extraValueAppend: -> 177 | if @isSetQuickPopup 178 | @params.extra ?= {} 179 | @params.extra.quick_popup = true 180 | @params.extra.popup = true 181 | @isSetQuickPopup = false 182 | 183 | # 결제창을 조립해서 만들고 부트페이로 결제 정보를 보낸다. 184 | # 보낸 이후에 app.bootpay.co.kr로 데이터를 전송한다. 185 | start: -> 186 | @progressMessageShow '' 187 | if @params.extra? and @params.extra.popup and @params.extra.quick_popup 188 | @doStartPopup( 189 | width: '300' 190 | height: '300' 191 | ) 192 | else 193 | @doStartIframe() 194 | 195 | # 기존 iFrame으로 결제를 시작한다 196 | doStartIframe: -> 197 | # 팝업이 떠있으면 일단 닫는다 198 | @popupInstance.close() if @popupInstance? 199 | document.getElementById(@iframeId).addEventListener('load', @progressMessageHide) 200 | document.bootpay_form.target = 'bootpay_inner_iframe' 201 | document.bootpay_form.submit() 202 | 203 | # 팝업으로 결제를 시작한다 204 | doStartPopup: (platform) -> 205 | unless @popupInstance? 206 | spec = if platform? and platform.width? and platform.width > 0 then "width=#{platform.width},height=#{platform.height},top=#{0},left=#{0},scrollbars=yes,toolbar=no, location=no, directories=no, status=no, menubar=no" else '' 207 | @popupInstance = window.open('about:blank', 'bootpayPopup', spec) 208 | @showPopupEventProgress() 209 | document.bootpay_form.target = 'bootpayPopup' 210 | document.bootpay_form.submit() 211 | 212 | # 팝업으로 시작하는 조건 부 async request 추가 213 | popupAsyncRequest: (conditions, method) -> 214 | return alert('비동기로 실행될 함수가 있어야 합니다.') unless method? 215 | # 먼저 팝업을 띄운다 216 | @startQuickPopup() if conditions 217 | # 함수 초기화 218 | @methods = {} 219 | method.call().then( 220 | (data) => 221 | @request(data, true) 222 | (e) => 223 | @clearEnvironment(true) 224 | @forceClose(e.message) 225 | ) 226 | @ 227 | 228 | # 사용자 promise 가 발생되기 전 선 팝업을 띄운다 229 | startQuickPopup: -> 230 | @isSetQuickPopup = true 231 | @expressPopupReady() 232 | 233 | # 미리 팝업을 준비한다 234 | expressPopupReady: -> 235 | if platform? and platform.width? and platform.width > 0 236 | spec = "width=#{platform.width},height=#{platform.height},top=#{0},left=#{0},scrollbars=yes,toolbar=no, location=no, directories=no, status=no, menubar=no" 237 | else 238 | spec = if @isMobile() then '' else "width=750,height=500,top=#{0},left=#{0},scrollbars=yes,toolbar=no, location=no, directories=no, status=no, menubar=no" 239 | @popupInstance = window.open('https://inapp.bootpay.co.kr/waiting', 'bootpayPopup', spec) 240 | 241 | # 결제할 iFrame 창을 만든다. 242 | iframeHtml: (url) -> 243 | """ 244 | 245 |
246 |
247 |
248 |
249 | 250 | 251 | 252 |
253 |
254 |
255 | 256 |
257 | 260 |
261 |
262 |
263 |
264 |
265 |
266 | 267 |
268 |

선택하신 결제는 팝업으로 결제가 시작됩니다. 결제를 시작할까요?

269 | 270 |
271 |
272 |
273 | """ 274 | 275 | # 팝업결제를 실행한다 276 | startPopup: -> 277 | @startPopupPaymentWindow(@popupData) 278 | 279 | # 간편결제 비밀번호 direct로 뜨게끔 280 | verifyPassword: (data = {}) -> 281 | @initializeEasySubmit( 282 | [@clientUrl(), 'easy', 'password', 'verify'].join('/') 283 | { 284 | user_token: data.userToken 285 | device_id: data.deviceId 286 | message: data.message 287 | } 288 | ) 289 | @ 290 | 291 | # 비밀번호 등록 292 | registerCard: (data = {}) -> 293 | @initializeEasySubmit( 294 | [@clientUrl(), 'easy', 'card', 'register'].join('/') 295 | { 296 | user_token: data.userToken 297 | device_id: data.deviceId 298 | message: data.message 299 | } 300 | ) 301 | @ 302 | 303 | changePassword: (data = {}) -> 304 | @initializeEasySubmit( 305 | [@clientUrl(), 'easy', 'password', 'change'].join('/') 306 | { 307 | user_token: data.userToken 308 | device_id: data.deviceId 309 | message: data.message 310 | } 311 | ) 312 | @ 313 | 314 | 315 | # 간편결제 관련 Form Submit 316 | initializeEasySubmit: (url, data = {}) -> 317 | encryptData = @encryptParams(data) 318 | document.body.insertAdjacentHTML( 319 | 'beforeend', 320 | """ 321 |
322 |
323 | 324 | 325 |
326 |
#{@iframeHtml('')}
327 |
328 | """ 329 | ) 330 | @bindEasyEvent() 331 | try document.body.classList.add('bootpay-open') 332 | catch then '' 333 | document.getElementById(@iframeId).style.setProperty('height', '100%') 334 | document.bootpayEasyForm.target = 'bootpay_inner_iframe' 335 | document.bootpayEasyForm.submit() 336 | 337 | 338 | # 결제를 승인한다 339 | transactionConfirm: (data) -> 340 | if @isConfirmLock() 341 | console.log 'Transaction Lock' 342 | else 343 | @setConfirmLock(true) 344 | if !data? or !data.receipt_id? 345 | alert '결제 승인을 하기 위해서는 receipt_id 값이 포함된 data값을 함께 보내야 합니다.' 346 | Logger.error 'this.transactionConfirm(data); 이렇게 confirm을 실행해주세요.' 347 | return 348 | 349 | html = """ 350 | 351 | 352 | """ 353 | document.getElementById('bootpay_confirm_form').innerHTML = html 354 | document.bootpay_confirm_form.action = "#{[@restUrl(), 'confirm'].join('/')}?#{@generateUUID()}" 355 | document.bootpay_confirm_form.target = 'bootpay_inner_iframe' 356 | document.bootpay_confirm_form.submit() 357 | @ 358 | } -------------------------------------------------------------------------------- /lib/extend/platform.coffee: -------------------------------------------------------------------------------- 1 | export default { 2 | # 모바일인지 구분 3 | isMobile: -> 4 | a = (navigator.userAgent || navigator.vendor || window.opera) 5 | /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|NetFront|Silk-Accelerated|(hpw|web)OS|Fennec|Minimo|Opera M(obi|ini)|Blazer|Dolfin|Dolphin|Skyfire|Zune/.test(a) 6 | 7 | isSafari: -> 8 | agent = window.navigator.userAgent.toLowerCase() 9 | agent.indexOf('safari') > -1 && agent.indexOf('chrome') is -1 10 | # 모바일 사파리인지 구분 11 | isMobileSafari: -> 12 | agent = window.navigator.userAgent 13 | (agent.match(/iPad/i) || agent.match(/iPhone/i)) && !agent.match(/CriOS/i)? 14 | 15 | getiOSVersion: -> 16 | try ((/CPU.*OS ([0-9_]{1,6})|(CPU like).*AppleWebKit.*Mobile/i.exec(window.navigator.userAgent))[1].replace(/_/g, '.')) || -1 17 | catch then -1 18 | 19 | # IE인지 검사한다 20 | isIE: -> 21 | window.navigator.userAgent.indexOf('MSIE') > 0 || window.navigator.userAgent.match(/Trident.*rv\:11\./)?.length 22 | # IE 버전 이하인지 검사한다 23 | isLtBrowserVersion: (version) -> 24 | sAgent = window.navigator.userAgent 25 | idx = sAgent.indexOf("MSIE") 26 | return false unless idx > 0 27 | version > parseInt(sAgent.substring(idx + 5, sAgent.indexOf(".", idx))) 28 | # IE 버전 blocking 29 | blockIEVersion: -> @isLtBrowserVersion @ieMinVersion 30 | # Platform String Return 31 | platformSymbol: -> 32 | if @isMobile() 33 | if @isMobileSafari() then 'ios' else 'android' 34 | else 35 | 'pc' 36 | } -------------------------------------------------------------------------------- /lib/extend/storage.coffee: -------------------------------------------------------------------------------- 1 | export default { 2 | #---------------------------------------------------------- 3 | # UUID가 없을 경우 바로 LocalStorage에 저장한다. 4 | # Comment by Gosomi 5 | # Date: 2018-04-29 6 | #---------------------------------------------------------- 7 | setReadyUUID: -> @setData 'uuid', @generateUUID() unless @getData('uuid')?.length 8 | #---------------------------------------------------------- 9 | # Local Storage에서 데이터를 저장한다. 10 | # Comment by Gosomi 11 | # Date: 2018-04-28 12 | #---------------------------------------------------------- 13 | setData: (key, value) -> 14 | try 15 | window.localStorage.setItem key, value 16 | catch 17 | @localStorage[key] = value 18 | #---------------------------------------------------------- 19 | # Local Storage에서 데이터를 가져온다. 20 | # Comment by Gosomi 21 | # Date: 2018-04-28 22 | #---------------------------------------------------------- 23 | getData: (key) -> 24 | try 25 | window.localStorage.getItem key 26 | catch 27 | @localStorage[key] 28 | } -------------------------------------------------------------------------------- /lib/logger.coffee: -------------------------------------------------------------------------------- 1 | export default Logger = 2 | logLevel: 1 3 | setLogLevel: (level) -> 4 | switch level 5 | when 1 # production 6 | @logLevel = 1 7 | when 2 # Warning 8 | @logLevel = 2 9 | when 3 # Info 10 | @logLevel = 3 11 | else 12 | @logLevel = 4 13 | 14 | debug: (msg) -> console.log msg if @logLevel >= 4 15 | info: (msg) -> console.info msg if @logLevel >= 3 16 | warn: (msg) -> console.warn msg if @logLevel >= 2 17 | error: (msg) -> console.error msg if @logLevel >= 1 -------------------------------------------------------------------------------- /lib/style.coffee: -------------------------------------------------------------------------------- 1 | style = document.createElement('style'); 2 | style.innerHTML = """ 3 | .bootpay-open { 4 | position: fixed !important; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | top: 0; 9 | height: 100vh !important; 10 | overflow: hidden !important; 11 | -webkit-overflow-scrolling: auto !important; 12 | } 13 | 14 | @media (min-width: 500px) { 15 | .bootpay-open { 16 | position: relative; 17 | background-color: transparent; 18 | } 19 | } 20 | 21 | .bootpay-loading-spinner { 22 | display: inline-block; 23 | width: 42px; 24 | height: 42px; 25 | vertical-align: middle; 26 | } 27 | 28 | .bootpay-loading-spinner .bootpay-circle { 29 | width: 42px; 30 | height: 42px; 31 | animation: bootpay-loading-rotate 2s linear infinite; 32 | vertical-align: middle; 33 | } 34 | 35 | .bootpay-loading-spinner .bootpay-circle .bootpay-path { 36 | stroke-dasharray: 90, 150; 37 | stroke-dashoffset: 0; 38 | stroke-width: 2; 39 | stroke: #ffffff; 40 | stroke-linecap: round; 41 | animation: bootpay-loading-dash 1.5s ease-in-out infinite; 42 | } 43 | 44 | @keyframes bootpay-loading-rotate { 45 | 100% { 46 | transform: rotate(360deg); 47 | } 48 | } 49 | 50 | @keyframes bootpay-loading-dash { 51 | 0% { 52 | stroke-dasharray: 1, 200; 53 | stroke-dashoffset: 0; 54 | } 55 | 50% { 56 | stroke-dasharray: 90, 150; 57 | stroke-dashoffset: -40px; 58 | } 59 | 100% { 60 | stroke-dasharray: 90, 150; 61 | stroke-dashoffset: -120px; 62 | } 63 | } 64 | 65 | @-webkit-keyframes bootpay-loading-rotate { 66 | 100% { 67 | transform: rotate(360deg); 68 | } 69 | } 70 | 71 | @-webkit-keyframes bootpay-loading-dash { 72 | 0% { 73 | stroke-dasharray: 1, 200; 74 | stroke-dashoffset: 0; 75 | } 76 | 50% { 77 | stroke-dasharray: 90, 150; 78 | stroke-dashoffset: -40px; 79 | } 80 | 100% { 81 | stroke-dasharray: 90, 150; 82 | stroke-dashoffset: -120px; 83 | } 84 | } 85 | 86 | .bootpay-modal-open { 87 | position: fixed; 88 | overflow: hidden; 89 | } 90 | 91 | .bootpay-window { 92 | display: block; 93 | position: fixed; 94 | left: 0; 95 | right: 0; 96 | top: 0; 97 | bottom: 0; 98 | background-color: rgba(0, 0, 0, 0.7); 99 | z-index: 30000; 100 | text-align: center; 101 | white-space: nowrap; 102 | -webkit-overflow-scrolling: touch; 103 | -webkit-transform: translate3d(0, 0, 0); 104 | } 105 | 106 | @media (min-width: 500px) { 107 | .bootpay-window { 108 | display: block; 109 | position: fixed; 110 | left: 0; 111 | right: 0; 112 | top: 0; 113 | bottom: 0; 114 | background-color: rgba(0, 0, 0, 0.7); 115 | z-index: 30000; 116 | text-align: center; 117 | white-space: nowrap; 118 | -webkit-overflow-scrolling: touch; 119 | -webkit-transform: translate3d(0, 0, 0); 120 | } 121 | } 122 | 123 | .bootpay-window.transparent-mode { 124 | background-color: transparent; 125 | } 126 | 127 | .bootpay-window .progress-message-window { 128 | display: none; 129 | position: absolute; 130 | left: 0; 131 | right: 0; 132 | top: 0; 133 | bottom: 0; 134 | z-index: 30000; 135 | text-align: center; 136 | white-space: nowrap; 137 | padding: 1em; 138 | } 139 | 140 | .bootpay-window .progress-message-window.over { 141 | z-index: 30002; 142 | } 143 | 144 | .bootpay-window .progress-message-window .progress-message { 145 | display: inline-block; 146 | text-align: center; 147 | max-width: 600px; 148 | border-radius: 3px; 149 | width: 100%; 150 | background-color: transparent; 151 | vertical-align: middle; 152 | } 153 | 154 | .bootpay-window .progress-message-window .close-message-box { 155 | display: inline-block; 156 | text-align: center; 157 | max-width: 400px; 158 | border-radius: 3px; 159 | width: 100%; 160 | white-space: normal; 161 | vertical-align: middle; 162 | } 163 | 164 | .bootpay-window .progress-message-window .close-message-box .close-popup { 165 | padding: 14px; 166 | background-color: #fff; 167 | color: #333; 168 | border-radius: 3px; 169 | } 170 | 171 | .bootpay-window .progress-message-window .close-message-box .close-popup .close-popup-header { 172 | position: relative; 173 | text-align: right; 174 | padding: 14px; 175 | } 176 | 177 | .bootpay-window .progress-message-window .close-message-box .close-popup .close-popup-header button.close-btn { 178 | position: absolute; 179 | top: -10px; 180 | right: -4px; 181 | box-shadow: none; 182 | font-size: 24px; 183 | outline: none; 184 | border: 0; 185 | background: transparent; 186 | padding: 0; 187 | cursor: pointer; 188 | } 189 | 190 | .bootpay-window .progress-message-window .close-message-box .close-popup h4.sub-title { 191 | font-size: 18px; 192 | padding: 0; 193 | margin-top: 7px; 194 | font-weight: 400; 195 | margin-bottom: 18px; 196 | text-align: left; 197 | } 198 | 199 | .bootpay-window .progress-message-window .close-message-box .close-popup button.close-payment-window { 200 | display: block; 201 | width: 100%; 202 | padding: 14px; 203 | border: 1px solid #5e72e4; 204 | border-radius: 2px; 205 | background-color: #5e72e4; 206 | border-radius: 5px; 207 | box-shadow: none; 208 | font-size: 16px; 209 | outline: none; 210 | color: #fff; 211 | cursor: pointer; 212 | } 213 | 214 | .bootpay-window .progress-message-window .close-message-box .close-popup button.close-payment-window.naverpay-btn { 215 | border: 1px solid #0fbc60; 216 | background-color: #1ec800; 217 | } 218 | 219 | 220 | @media (min-width: 500px) { 221 | .bootpay-window .progress-message-window .progress-message { 222 | display: inline-block; 223 | width: 400px; 224 | vertical-align: middle; 225 | margin-top: 0; 226 | } 227 | } 228 | 229 | .bootpay-window .progress-message-window .progress-message .bootpay-text { 230 | margin-top: 1rem; 231 | } 232 | 233 | .bootpay-window .progress-message-window .progress-message .bootpay-popup-close { 234 | position: absolute; 235 | right: 1rem; 236 | top: 0; 237 | } 238 | 239 | .bootpay-window .progress-message-window .progress-message .bootpay-popup-close button { 240 | background: transparent; 241 | border: 0; 242 | outline: 0; 243 | -webkit-appearance: none; 244 | cursor: pointer; 245 | font-size: 28px; 246 | color: #fff; 247 | padding: 0; 248 | margin: 0; 249 | } 250 | 251 | .bootpay-window .progress-message-window .progress-message .bootpay-text span.bootpay-inner-text { 252 | font-size: 14px; 253 | font-weight: 400; 254 | color: #ffffff; 255 | } 256 | 257 | .bootpay-window:before, .progress-message-window:before { 258 | display: inline-block; 259 | vertical-align: middle; 260 | height: 100%; 261 | content: ' '; 262 | background: transparent; 263 | } 264 | 265 | .bootpay-window iframe { 266 | position: relative; 267 | display: inline-block; 268 | width: 100%; 269 | height: 100%; 270 | border: none; 271 | outline: none; 272 | background-color: transparent; 273 | z-index: 30001; 274 | } 275 | 276 | @media (min-width: 500px) { 277 | .bootpay-window iframe { 278 | position: relative; 279 | display: inline-block; 280 | width: 400px; 281 | height: 100%; 282 | max-height: 760px; 283 | vertical-align: middle; 284 | background-color: transparent; 285 | } 286 | } 287 | """ 288 | document.head.appendChild(style) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootpay-js", 3 | "version": "3.3.6", 4 | "description": "Bootpay Javasrcipt Library", 5 | "main": "lib/bootpay.js", 6 | "files": [ 7 | "lib/*.js", 8 | "lib/extend/*.js" 9 | ], 10 | "devDependencies": { 11 | "coffee-loader": "^0.9.0", 12 | "coffeescript": "^2.5.1", 13 | "uglifyjs-webpack-plugin": "^2.2.0", 14 | "webpack": "^4.41.6", 15 | "webpack-cli": "^3.3.11", 16 | "webpack-dev-server": "^3.10.3", 17 | "css-loader": "^3.4.2", 18 | "style-loader": "^1.1.3" 19 | }, 20 | "dependencies": { 21 | "babel-core": "^6.26.3", 22 | "object-assign": "^4.1.1", 23 | "es6-promise": "^4.2.6", 24 | "babel-preset-es2015": "^6.24.1", 25 | "crypto-js": "^3.1.9-1", 26 | "npm": "^6.1.0", 27 | "superagent": "^5.2.1" 28 | }, 29 | "scripts": { 30 | "prepublish": "./prepublish.sh" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/bootpay/bootpay_js.git" 35 | }, 36 | "keywords": [ 37 | "결제", 38 | "payment", 39 | "결제연동", 40 | "PG연동", 41 | "PG", 42 | "부트페이", 43 | "bootpay" 44 | ], 45 | "author": "Bootpay", 46 | "license": "ISC", 47 | "bugs": { 48 | "url": "https://github.com/bootpay/bootpay_js/issues" 49 | }, 50 | "homepage": "https://docs.bootpay.co.kr", 51 | "urls": { 52 | "restUrl": { 53 | "development": "https://dev-api.bootpay.co.kr", 54 | "test": "https://test-api.bootpay.co.kr", 55 | "stage": "https://stage-api.bootpay.co.kr", 56 | "production": "https://api.bootpay.co.kr" 57 | }, 58 | "clientUrl": { 59 | "development": "https://dev-app.bootpay.co.kr", 60 | "test": "https://test-app.bootpay.co.kr", 61 | "stage": "https://stage-app.bootpay.co.kr", 62 | "production": "https://app.bootpay.co.kr" 63 | }, 64 | "analyticsUrl": { 65 | "development": "https://dev-analytics.bootpay.co.kr", 66 | "test": "https://test-analytics.bootpay.co.kr", 67 | "stage": "https://stage-analytics.bootpay.co.kr", 68 | "production": "https://analytics.bootpay.co.kr" 69 | }, 70 | "naverpayZzimUrl": { 71 | "development": { 72 | "pc": "https://test-pay.naver.com/customer/wishlistPopup.nhn", 73 | "mobile": "https://test-m.pay.naver.com/mobile/customer/wishList.nhn" 74 | }, 75 | "test": { 76 | "pc": "https://test-pay.naver.com/customer/wishlistPopup.nhn", 77 | "mobile": "https://test-m.pay.naver.com/mobile/customer/wishList.nhn" 78 | }, 79 | "production": { 80 | "pc": "https://pay.naver.com/customer/wishlistPopup.nhn", 81 | "mobile": "https://m.pay.naver.com/mobile/customer/wishList.nhn" 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -f lib/extend/*js 3 | rm -f lib/*js 4 | coffee --transpile -o lib/extend -c lib/extend/*.coffee 5 | coffee --transpile -o lib -c lib/*.coffee -------------------------------------------------------------------------------- /public/waiting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 부트페이 결제 진행중 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 | 결제창을 불러오는 중입니다. 24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /public/waiting/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | .bootpay-open { 2 | position: fixed !important; 3 | left: 0; 4 | right: 0; 5 | bottom: 0; 6 | top: 0; 7 | height: 100vh !important; 8 | overflow: hidden !important; 9 | -webkit-overflow-scrolling: auto !important; 10 | } 11 | 12 | .bootpay-loading-spinner { 13 | display: inline-block; 14 | width: 42px; 15 | height: 42px; 16 | vertical-align: middle; 17 | } 18 | 19 | .bootpay-loading-spinner .bootpay-circle { 20 | width: 42px; 21 | height: 42px; 22 | animation: bootpay-loading-rotate 2s linear infinite; 23 | vertical-align: middle; 24 | } 25 | 26 | .bootpay-loading-spinner .bootpay-circle .bootpay-path { 27 | stroke-dasharray: 90, 150; 28 | stroke-dashoffset: 0; 29 | stroke-width: 2; 30 | stroke: #ffffff; 31 | stroke-linecap: round; 32 | animation: bootpay-loading-dash 1.5s ease-in-out infinite; 33 | } 34 | 35 | @keyframes bootpay-loading-rotate { 36 | 100% { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | 41 | @keyframes bootpay-loading-dash { 42 | 0% { 43 | stroke-dasharray: 1, 200; 44 | stroke-dashoffset: 0; 45 | } 46 | 50% { 47 | stroke-dasharray: 90, 150; 48 | stroke-dashoffset: -40px; 49 | } 50 | 100% { 51 | stroke-dasharray: 90, 150; 52 | stroke-dashoffset: -120px; 53 | } 54 | } 55 | 56 | @-webkit-keyframes bootpay-loading-rotate { 57 | 100% { 58 | transform: rotate(360deg); 59 | } 60 | } 61 | 62 | @-webkit-keyframes bootpay-loading-dash { 63 | 0% { 64 | stroke-dasharray: 1, 200; 65 | stroke-dashoffset: 0; 66 | } 67 | 50% { 68 | stroke-dasharray: 90, 150; 69 | stroke-dashoffset: -40px; 70 | } 71 | 100% { 72 | stroke-dasharray: 90, 150; 73 | stroke-dashoffset: -120px; 74 | } 75 | } 76 | 77 | .bootpay-window { 78 | display: block; 79 | position: fixed; 80 | left: 0; 81 | right: 0; 82 | top: 0; 83 | bottom: 0; 84 | background-color: rgba(0, 0, 0, 0.7); 85 | z-index: 30000; 86 | text-align: center; 87 | white-space: nowrap; 88 | -webkit-overflow-scrolling: touch; 89 | -webkit-transform: translate3d(0, 0, 0); 90 | } 91 | 92 | @media (min-width: 500px) { 93 | .bootpay-window { 94 | display: block; 95 | position: fixed; 96 | left: 0; 97 | right: 0; 98 | top: 0; 99 | bottom: 0; 100 | background-color: rgba(0, 0, 0, 0.7); 101 | z-index: 30000; 102 | text-align: center; 103 | white-space: nowrap; 104 | -webkit-overflow-scrolling: touch; 105 | -webkit-transform: translate3d(0, 0, 0); 106 | } 107 | } 108 | 109 | .bootpay-window .progress-message-window { 110 | position: absolute; 111 | left: 0; 112 | right: 0; 113 | top: 0; 114 | bottom: 0; 115 | z-index: 30000; 116 | text-align: center; 117 | white-space: nowrap; 118 | padding: 1em; 119 | } 120 | 121 | .bootpay-window .progress-message-window.over { 122 | z-index: 30002; 123 | } 124 | 125 | .bootpay-window .progress-message-window .progress-message { 126 | display: inline-block; 127 | text-align: center; 128 | max-width: 600px; 129 | border-radius: 3px; 130 | width: 100%; 131 | background-color: transparent; 132 | vertical-align: middle; 133 | } 134 | 135 | .bootpay-window .progress-message-window .close-message-box { 136 | display: inline-block; 137 | text-align: center; 138 | max-width: 400px; 139 | border-radius: 3px; 140 | width: 100%; 141 | white-space: normal; 142 | vertical-align: middle; 143 | } 144 | 145 | .bootpay-window .progress-message-window .close-message-box .close-popup { 146 | padding: 14px; 147 | background-color: #fff; 148 | color: #333; 149 | border-radius: 3px; 150 | } 151 | 152 | .bootpay-window .progress-message-window .close-message-box .close-popup .close-popup-header { 153 | position: relative; 154 | text-align: right; 155 | padding: 14px; 156 | } 157 | 158 | .bootpay-window .progress-message-window .close-message-box .close-popup .close-popup-header button.close-btn { 159 | position: absolute; 160 | top: -10px; 161 | right: -4px; 162 | box-shadow: none; 163 | font-size: 24px; 164 | outline: none; 165 | border: 0; 166 | background: transparent; 167 | padding: 0; 168 | cursor: pointer; 169 | } 170 | 171 | .bootpay-window .progress-message-window .close-message-box .close-popup h4.sub-title { 172 | font-size: 18px; 173 | padding: 0; 174 | margin-top: 7px; 175 | font-weight: 400; 176 | margin-bottom: 18px; 177 | text-align: left; 178 | } 179 | 180 | .bootpay-window .progress-message-window .close-message-box .close-popup button.close-payment-window { 181 | display: block; 182 | width: 100%; 183 | padding: 14px; 184 | border: 1px solid #5e72e4; 185 | border-radius: 2px; 186 | background-color: #5e72e4; 187 | border-radius: 5px; 188 | box-shadow: none; 189 | font-size: 16px; 190 | outline: none; 191 | color: #fff; 192 | cursor: pointer; 193 | } 194 | 195 | .bootpay-window .progress-message-window .close-message-box .close-popup button.close-payment-window.naverpay-btn { 196 | border: 1px solid #0fbc60; 197 | background-color: #1ec800; 198 | } 199 | 200 | 201 | @media (min-width: 500px) { 202 | .bootpay-window .progress-message-window .progress-message { 203 | display: inline-block; 204 | width: 400px; 205 | vertical-align: middle; 206 | margin-top: 0; 207 | } 208 | } 209 | 210 | .bootpay-window .progress-message-window .progress-message .bootpay-text { 211 | margin-top: 1rem; 212 | } 213 | 214 | .bootpay-window .progress-message-window .progress-message .bootpay-popup-close { 215 | position: absolute; 216 | right: 1rem; 217 | top: 0.5rem; 218 | } 219 | 220 | .bootpay-window .progress-message-window .progress-message .bootpay-popup-close button { 221 | background: transparent; 222 | border: 0; 223 | outline: 0; 224 | -webkit-appearance: none; 225 | cursor: pointer; 226 | font-size: 28px; 227 | color: #fff; 228 | padding: 0; 229 | margin: 0; 230 | } 231 | 232 | .bootpay-window .progress-message-window .progress-message .bootpay-text span.bootpay-inner-text { 233 | font-size: 14px; 234 | font-weight: 400; 235 | color: #ffffff; 236 | } 237 | 238 | .bootpay-window:before, .progress-message-window:before { 239 | display: inline-block; 240 | vertical-align: middle; 241 | height: 100%; 242 | content: ' '; 243 | background: transparent; 244 | } 245 | 246 | @media (min-width: 500px) { 247 | .bootpay-window iframe { 248 | position: relative; 249 | display: inline-block; 250 | width: 400px; 251 | height: 100%; 252 | max-height: 760px; 253 | vertical-align: middle; 254 | background-color: transparent; 255 | } 256 | } -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import BootPay from 'bootpay-js' 2 | 3 | import Router from 'next/router' 4 | 5 | import * as ROUTES from '~/constants/routes' 6 | 7 | import paymentVerify from './paymentVerify' 8 | 9 | import type { 10 | 11 | BootPayCancelResponse, 12 | 13 | BootPayConfirmResponse, 14 | 15 | BootPayDefaultResponse, 16 | 17 | BootPayDoneResponse, 18 | 19 | BootPayErrorResponse, 20 | 21 | BootPayReadyResponse, 22 | 23 | } from './types' 24 | 25 | import { PaymentFormValues } from '../../../types' 26 | 27 | import paymentCancel from './paymentCancel' 28 | 29 | import getPaymentForm from './getPaymentInfo' 30 | 31 | async function paymentOnRequest(formValues: PaymentFormValues): Promise { 32 | 33 | const paymentDetailsInfo = getPaymentForm(formValues) 34 | 35 | console.log('bootPay Start') 36 | 37 | console.log('BootPay : ', BootPay) 38 | 39 | console.log('paymentDetailsInfo : ', paymentDetailsInfo) 40 | 41 | BootPay.request(paymentDetailsInfo) 42 | 43 | .error(async function (data: BootPayErrorResponse) { 44 | 45 | //결제 진행시 에러가 발생하면 수행됩니다. 46 | 47 | console.log('error', data) 48 | 49 | await handleBootPayOnError(data) 50 | 51 | }) 52 | 53 | .cancel(async function (data: BootPayCancelResponse) { 54 | 55 | //결제가 취소되면 수행됩니다. 56 | 57 | await handleBootPayOnCancel(data) 58 | 59 | }) 60 | 61 | .ready(function (data: BootPayReadyResponse) { 62 | 63 | // 가상계좌 입금 계좌번호가 발급되면 호출되는 함수입니다. 64 | 65 | console.log('ready', data) 66 | 67 | }) 68 | 69 | .confirm(async function (data: BootPayConfirmResponse) { 70 | 71 | //결제가 실행되기 전에 수행되며, 주로 재고를 확인하는 로직이 들어갑니다. 72 | 73 | //주의 - 카드 수기결제일 경우 이 부분이 실행되지 않습니다. 74 | 75 | await handleBootPayOnConfirm(data) 76 | 77 | }) 78 | 79 | .close(function (data: BootPayDefaultResponse) { 80 | 81 | // 결제창이 닫힐때 수행됩니다. (성공,실패,취소에 상관없이 모두 수행됨) 82 | 83 | console.log('close', data) 84 | 85 | }) 86 | 87 | .done(async function (data: BootPayDoneResponse) { 88 | 89 | //결제가 정상적으로 완료되면 수행됩니다 90 | 91 | //비즈니스 로직을 수행하기 전에 결제 유효성 검증을 하시길 추천합니다. 92 | 93 | console.log('결제 성공') 94 | 95 | await handleBootPayOnDone(data, formValues) 96 | 97 | }) 98 | 99 | } 100 | 101 | async function handleBootPayOnError(data: BootPayErrorResponse) { 102 | 103 | // code -13001 104 | 105 | if (data.code === -13001) { 106 | 107 | alert(data.message + '\n' + '은행 마감시간 확인 후 다시 시도해 주세요') 108 | 109 | Router.reload() 110 | 111 | } 112 | 113 | //console.log('error', data); 114 | 115 | } 116 | 117 | async function handleBootPayOnCancel(data: BootPayCancelResponse) { 118 | 119 | await paymentCancel(data.receipt_id) 120 | 121 | alert('결제가 취소되었습니다.') 122 | 123 | //console.log('cancel', data); 124 | 125 | } 126 | 127 | async function handleBootPayOnConfirm(data: BootPayConfirmResponse) { 128 | 129 | //console.log('confirm', data); 130 | 131 | const enable = true // 재고 수량 관리 로직 혹은 다른 처리 132 | 133 | if (enable) { 134 | 135 | BootPay.transactionConfirm(data) // 조건이 맞으면 승인 처리를 한다. 136 | 137 | } else { 138 | 139 | BootPay.removePaymentWindow() // 조건이 맞지 않으면 결제 창을 닫고 결제를 승인하지 않는다. 140 | 141 | } 142 | 143 | } 144 | 145 | async function handleBootPayOnDone( 146 | data: BootPayDoneResponse, 147 | formValues: PaymentFormValues, 148 | ): Promise { 149 | 150 | // verify payment as bootpay recommended 151 | 152 | const { 153 | 154 | isValid, 155 | 156 | errorMessage, 157 | 158 | redirectUrlParam, 159 | 160 | receiptUrl, 161 | 162 | } = await paymentVerify(data, formValues) 163 | 164 | if (!isValid) { 165 | 166 | // cancel payment if its not valid 167 | 168 | await paymentCancel(data.receipt_id) 169 | 170 | alert('결제가 취소되었습니다.' + '\n' + errorMessage) 171 | 172 | Router.reload() 173 | 174 | return 175 | 176 | } 177 | 178 | alert('결제가 완료되었습니다.') 179 | 180 | Router.replace( 181 | `${ ROUTES.ARTWORK_PAYMENT_SUCCESS }/?id=${ redirectUrlParam }&receipt_url=${ receiptUrl }`, 182 | ) 183 | 184 | } 185 | 186 | export default paymentOnRequest -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | module.exports = { 3 | entry: './lib/bootpay.coffee', 4 | output: { 5 | path: __dirname, 6 | filename: "bootpay-latest.js" 7 | }, 8 | resolve: { 9 | extensions: ['.js', '.css', '.sass', '.coffee', '.json'] 10 | }, 11 | devServer: { 12 | port: 3001, 13 | public: 'd-cdn.bootapi.com', 14 | inline: false, 15 | hot: false, 16 | disableHostCheck: false, 17 | host: '0.0.0.0' 18 | }, 19 | optimization: { 20 | minimizer: [ 21 | new UglifyJsPlugin({ 22 | uglifyOptions: { 23 | compress: true, 24 | ecma: 5, 25 | mangle: true, 26 | output: { 27 | comments: false, 28 | beautify: false 29 | }, 30 | cache: true, 31 | parallel: true, 32 | sourceMap: (process.env.MODE_ENV !== 'production') 33 | } 34 | }) 35 | ] 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.coffee(\.erb)?$/, 41 | use: [{ 42 | loader: 'coffee-loader', 43 | options: { 44 | // literate: true, 45 | transpile: { 46 | presets: ['es2015'] 47 | } 48 | } 49 | }] 50 | }, 51 | { 52 | test: /\.css/, 53 | use: ['style-loader', 'css-loader'] 54 | }, 55 | { 56 | test: /\.sass$/, 57 | use: [{ 58 | loader: "style-loader" // creates style nodes from JS strings 59 | }, { 60 | loader: "css-loader" // translates CSS into CommonJS 61 | }, { 62 | loader: "sass-loader" // compiles Sass to CSS 63 | }] 64 | } 65 | ] 66 | } 67 | }; --------------------------------------------------------------------------------