토스페이 캐시 적립/가상계좌 입금 콜백 구현 및 라이브 키 적용
- 카드/간편결제 승인 시 mj_cash 적립 + 회원 잔액(USER_MONEY)/포인트 갱신 - 가상계좌 입금 웹훅(/web/toss/webhook/deposit.do) 추가: 입금완료(DONE) 시 적립, 멱등 처리 - 토스 결제조회 API로 입금상태 재확인(위변조 방지) - 토스 시크릿/클라이언트 키를 globals_*.properties 로 분리 (라이브 위젯 키 적용, PG 심사용) - 웹훅 URL 시큐리티 인증 제외(context-security.xml) Co-Authored-By: Claude Opus 4.8
@7152e155426e252cb5413ade7fb3915ec01fc689
--- src/main/java/itn/let/mjo/pay/service/MjonPayTossService.java
+++ src/main/java/itn/let/mjo/pay/service/MjonPayTossService.java
... | ... | @@ -9,6 +9,14 @@ |
| 9 | 9 |
public interface MjonPayTossService {
|
| 10 | 10 |
Map<String, Object> processTossConfirm(String paymentKey, String orderId, String amount, LoginVO loginVO) throws Exception; |
| 11 | 11 |
|
| 12 |
+ /** |
|
| 13 |
+ * 토스페이먼츠 가상계좌 입금 콜백(웹훅) 처리. |
|
| 14 |
+ * 입금이 완료(DONE)된 주문에 대해 mj_pg 상태를 결제완료로 바꾸고 캐시/포인트를 적립한다. |
|
| 15 |
+ * @param orderId 상점 주문번호 (MOID) |
|
| 16 |
+ * @return 처리결과(credited: 실제 적립 여부, message) |
|
| 17 |
+ */ |
|
| 18 |
+ Map<String, Object> processTossVbankDeposit(String orderId) throws Exception; |
|
| 19 |
+ |
|
| 12 | 20 |
// public int selectBLineMberCnt(String mberId) throws Exception; |
| 13 | 21 |
// |
| 14 | 22 |
// public int insertPrePayInfo(MjonPrePayVO mjonPrePayVO) throws Exception; |
--- src/main/java/itn/let/mjo/pay/service/impl/MjonPayDAO.java
+++ src/main/java/itn/let/mjo/pay/service/impl/MjonPayDAO.java
... | ... | @@ -201,6 +201,11 @@ |
| 201 | 201 |
return update("mjonPayDAO.updateMjonPgStatus", refundVO);
|
| 202 | 202 |
} |
| 203 | 203 |
|
| 204 |
+ // 토스 가상계좌 입금완료 처리: PG_STATUS 0(입금대기) → 1(결제완료). 멱등 처리를 위해 0인 건만 갱신. |
|
| 205 |
+ public int updateTossVbankDeposit(MjonPayVO mjonPayVO) throws Exception {
|
|
| 206 |
+ return update("mjonPayDAO.updateTossVbankDeposit", mjonPayVO);
|
|
| 207 |
+ } |
|
| 208 |
+ |
|
| 204 | 209 |
@SuppressWarnings("unchecked")
|
| 205 | 210 |
public List<MjonPayVO> selectPayDayChart(MjonPayVO mjonPayVO) throws Exception{
|
| 206 | 211 |
return (List<MjonPayVO>)list("mjonPayDAO.selectPayDayChart", mjonPayVO);
|
--- src/main/java/itn/let/mjo/pay/service/impl/MjonPayTossServiceImpl.java
+++ src/main/java/itn/let/mjo/pay/service/impl/MjonPayTossServiceImpl.java
... | ... | @@ -1,16 +1,21 @@ |
| 1 | 1 |
package itn.let.mjo.pay.service.impl; |
| 2 | 2 |
|
| 3 | 3 |
import egovframework.rte.fdl.cmmn.EgovAbstractServiceImpl; |
| 4 |
+import egovframework.rte.fdl.idgnr.EgovIdGnrService; |
|
| 4 | 5 |
import itn.com.cmm.LoginVO; |
| 6 |
+import itn.let.mjo.pay.service.MjonPayService; |
|
| 5 | 7 |
import itn.let.mjo.pay.service.MjonPayTossService; |
| 6 | 8 |
|
| 7 | 9 |
import javax.annotation.Resource; |
| 8 | 10 |
import java.io.*; |
| 9 | 11 |
import java.util.Map; |
| 10 | 12 |
|
| 13 |
+import itn.let.mjo.msgdata.service.MjonMsgDataService; |
|
| 11 | 14 |
import itn.let.mjo.pay.service.MjonPayVO; |
| 15 |
+import itn.let.sym.site.service.JoinSettingVO; |
|
| 12 | 16 |
import itn.let.uat.uia.service.impl.MberManageDAO; |
| 13 | 17 |
import itn.let.uss.umt.service.MberManageVO; |
| 18 |
+import org.springframework.beans.factory.annotation.Value; |
|
| 14 | 19 |
import org.springframework.stereotype.Service; |
| 15 | 20 |
|
| 16 | 21 |
import java.nio.charset.StandardCharsets; |
... | ... | @@ -32,9 +37,30 @@ |
| 32 | 37 |
@Resource(name="mjonPayDAO") |
| 33 | 38 |
private MjonPayDAO mjonPayDAO; |
| 34 | 39 |
|
| 40 |
+ /** 캐시/회원잔액 적립 공용 서비스 (insertCash → mj_cash + LETTNGNRLMBER.USER_MONEY) */ |
|
| 41 |
+ @Resource(name="mjonPayService") |
|
| 42 |
+ private MjonPayService mjonPayService; |
|
| 43 |
+ |
|
| 44 |
+ /** 포인트 ID 생성기 */ |
|
| 45 |
+ @Resource(name="egovMjonPointIdGnrService") |
|
| 46 |
+ private EgovIdGnrService idgenMjonPointId; |
|
| 47 |
+ |
|
| 48 |
+ /** 가입설정(포인트 적립비율) 조회 */ |
|
| 49 |
+ @Resource(name="MjonMsgDataService") |
|
| 50 |
+ private MjonMsgDataService mjonMsgDataService; |
|
| 51 |
+ |
|
| 35 | 52 |
/** mberManageDAO */ |
| 36 | 53 |
@Resource(name="mberManageDAO") |
| 37 | 54 |
private MberManageDAO mberManageDAO; |
| 55 |
+ |
|
| 56 |
+ /** 토스 시크릿 키 (globals_*.properties, 콜론 없이 키 본문만 저장 → 인증 시 코드에서 ':' 부착) */ |
|
| 57 |
+ @Value("#{globalSettings['Globals.pay.toss.secretKey']}")
|
|
| 58 |
+ private String tossSecretKey; |
|
| 59 |
+ |
|
| 60 |
+ /** 토스 Basic 인증 헤더값 = Base64(secretKey + ":") */ |
|
| 61 |
+ private String tossBasicAuth() {
|
|
| 62 |
+ return Base64.getEncoder().encodeToString((tossSecretKey + ":").getBytes(StandardCharsets.UTF_8)); |
|
| 63 |
+ } |
|
| 38 | 64 |
|
| 39 | 65 |
/** |
| 40 | 66 |
* 토스페이먼츠 결제 승인 및 내역 저장 |
... | ... | @@ -48,8 +74,7 @@ |
| 48 | 74 |
public Map<String, Object> processTossConfirm(String paymentKey, String orderId, String amount, LoginVO loginVO) throws Exception {
|
| 49 | 75 |
|
| 50 | 76 |
// 1. 토스 승인 API 호출 설정 |
| 51 |
- String secretKey = "test_gsk_GjLJoQ1aVZ5nzl22xOql3w6KYe2R:"; // 시크릿 키 (뒤에 콜론 필수) |
|
| 52 |
- String encodedKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8)); |
|
| 77 |
+ String encodedKey = tossBasicAuth(); |
|
| 53 | 78 |
|
| 54 | 79 |
URL url = new URL("https://api.tosspayments.com/v1/payments/confirm");
|
| 55 | 80 |
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
... | ... | @@ -111,9 +136,11 @@ |
| 111 | 136 |
payVO.setResultMsg("정상처리 되었습니다."); // #resultMsg#
|
| 112 | 137 |
payVO.setAfterPayYn("N"); // #afterPayYn# (선불 고정)
|
| 113 | 138 |
|
| 114 |
- // CASH(공급가액) 계산: 부가세 제외 금액 |
|
| 139 |
+ // CASH(공급가액) 계산: 부가세 제외 금액 (기존 시스템과 동일 산식: 결제금액/11*10) |
|
| 140 |
+ // 토스 응답의 suppliedAmount와 1원 단위로 어긋나 세금계산서/정산 금액과 불일치하는 것을 방지하기 위해 |
|
| 141 |
+ // 기존 setCashVatNotIncluded() 와 동일한 산식으로 통일한다. |
|
| 115 | 142 |
long totalAmt = resObj.getLong("totalAmount");
|
| 116 |
- long cashAmount = resObj.optLong("suppliedAmount", Math.round(totalAmt / 1.1));
|
|
| 143 |
+ long cashAmount = Math.round(totalAmt / 11.0 * 10); |
|
| 117 | 144 |
payVO.setCash(Double.parseDouble(String.valueOf(cashAmount))); // #cash# |
| 118 | 145 |
|
| 119 | 146 |
// 4. 결제 수단별 상세 분기 처리 |
... | ... | @@ -210,6 +237,14 @@ |
| 210 | 237 |
// 5. DB INSERT 실행 |
| 211 | 238 |
mjonPayDAO.insertMjPg(payVO); |
| 212 | 239 |
|
| 240 |
+ // 5-1. 캐시/포인트 적립 |
|
| 241 |
+ // - 카드/간편결제: 즉시 결제완료(pgStatus=1)이므로 바로 적립 |
|
| 242 |
+ // - 가상계좌(VBANK): 아직 입금 전(pgStatus=0)이므로 적립하지 않음. |
|
| 243 |
+ // 실제 입금이 완료되면 토스 입금 콜백(processTossVbankDeposit)에서 적립한다. |
|
| 244 |
+ if (!"VBANK".equals(payVO.getPayMethod())) {
|
|
| 245 |
+ creditCashAndPoint(payVO, loginVO.getId()); |
|
| 246 |
+ } |
|
| 247 |
+ |
|
| 213 | 248 |
// 컨트롤러 응답용 결과 Map 구성 |
| 214 | 249 |
Map<String, Object> result = new HashMap<>(); |
| 215 | 250 |
result.put("PG_STATUS", payVO.getPgStatus());
|
... | ... | @@ -223,10 +258,141 @@ |
| 223 | 258 |
} |
| 224 | 259 |
|
| 225 | 260 |
/** |
| 226 |
- * 입금 대기 중인 가상계좌 조회 (PayView용) |
|
| 261 |
+ * 토스페이먼츠 가상계좌 입금 콜백(웹훅) 처리. |
|
| 262 |
+ * 입금 완료(DONE)된 주문에 대해서만 mj_pg 상태를 결제완료로 바꾸고 캐시/포인트를 적립한다. |
|
| 263 |
+ * 중복 콜백/재시도에 대비해 멱등(idempotent)하게 동작한다. |
|
| 227 | 264 |
*/ |
| 228 |
-/* @Override |
|
| 229 |
- public Map<String, Object> selectWaitingVBank(String userId) throws Exception {
|
|
| 230 |
- return mjonPayDAO.selectWaitingVBank(userId); |
|
| 231 |
- }*/ |
|
| 265 |
+ @Override |
|
| 266 |
+ public Map<String, Object> processTossVbankDeposit(String orderId) throws Exception {
|
|
| 267 |
+ |
|
| 268 |
+ Map<String, Object> result = new HashMap<>(); |
|
| 269 |
+ result.put("credited", false);
|
|
| 270 |
+ |
|
| 271 |
+ if (orderId == null || orderId.isEmpty()) {
|
|
| 272 |
+ result.put("message", "orderId 없음");
|
|
| 273 |
+ return result; |
|
| 274 |
+ } |
|
| 275 |
+ |
|
| 276 |
+ // 1. 우리 DB의 결제내역 조회 (없으면 우리쪽 주문이 아님) |
|
| 277 |
+ MjonPayVO dbPay = mjonPayDAO.selectPayInfoByMoid(orderId); |
|
| 278 |
+ if (dbPay == null) {
|
|
| 279 |
+ result.put("message", "존재하지 않는 주문번호: " + orderId);
|
|
| 280 |
+ return result; |
|
| 281 |
+ } |
|
| 282 |
+ // 이미 처리(결제완료)된 주문이면 중복 콜백 → 무시 |
|
| 283 |
+ if ("1".equals(dbPay.getPgStatus())) {
|
|
| 284 |
+ result.put("message", "이미 처리된 주문: " + orderId);
|
|
| 285 |
+ return result; |
|
| 286 |
+ } |
|
| 287 |
+ |
|
| 288 |
+ // 2. 토스 결제조회 API로 실제 상태 재확인 (위변조 방지: 콜백 본문만 신뢰하지 않음) |
|
| 289 |
+ JSONObject payInfo = callTossPaymentByOrderId(orderId); |
|
| 290 |
+ String status = payInfo.optString("status");
|
|
| 291 |
+ if (!"DONE".equals(status)) {
|
|
| 292 |
+ // 입금 전(WAITING_FOR_DEPOSIT)이거나 취소/만료 등 → 적립하지 않음 |
|
| 293 |
+ result.put("message", "입금완료 상태 아님: " + status);
|
|
| 294 |
+ return result; |
|
| 295 |
+ } |
|
| 296 |
+ |
|
| 297 |
+ // 3. mj_pg 상태를 결제완료(1)로 변경 — PG_STATUS='0' 조건으로 멱등 처리 |
|
| 298 |
+ MjonPayVO updVO = new MjonPayVO(); |
|
| 299 |
+ updVO.setMoid(orderId); |
|
| 300 |
+ int updated = mjonPayDAO.updateTossVbankDeposit(updVO); |
|
| 301 |
+ if (updated == 0) {
|
|
| 302 |
+ // 동시 콜백 등으로 이미 다른 요청이 처리함 → 중복 적립 방지 |
|
| 303 |
+ result.put("message", "상태 변경 대상 없음(이미 처리됨): " + orderId);
|
|
| 304 |
+ return result; |
|
| 305 |
+ } |
|
| 306 |
+ |
|
| 307 |
+ // 4. 캐시/포인트 적립 |
|
| 308 |
+ long totalAmt = payInfo.getLong("totalAmount");
|
|
| 309 |
+ dbPay.setAmt(String.valueOf(totalAmt)); |
|
| 310 |
+ dbPay.setPayMethod("VBANK");
|
|
| 311 |
+ creditCashAndPoint(dbPay, dbPay.getUserId()); |
|
| 312 |
+ |
|
| 313 |
+ result.put("credited", true);
|
|
| 314 |
+ result.put("message", "입금 적립 완료: " + orderId);
|
|
| 315 |
+ return result; |
|
| 316 |
+ } |
|
| 317 |
+ |
|
| 318 |
+ /** |
|
| 319 |
+ * 캐시/포인트 적립 공통 처리. |
|
| 320 |
+ * - 캐시: 공급가액(결제금액/11*10)을 mj_cash 에 적립하고 회원 잔액(USER_MONEY) 갱신 |
|
| 321 |
+ * - 포인트: 문자 미할인 회원만 가입설정 비율만큼 mj_point 에 적립하고 회원 포인트 갱신 |
|
| 322 |
+ * |
|
| 323 |
+ * 주의: 첫결제 이벤트 회원에 대한 포인트 0원 처리/이벤트 회원정보 갱신 등의 특수 로직은 |
|
| 324 |
+ * 포함하지 않는다. (해당 처리가 필요하면 MjonPayServiceImpl 의 일반 결제 흐름 참고) |
|
| 325 |
+ * |
|
| 326 |
+ * @param payVO amt(결제금액, 부가세포함)/moid/payMethod 가 세팅된 VO |
|
| 327 |
+ * @param userId 적립 대상 회원 ID |
|
| 328 |
+ */ |
|
| 329 |
+ private void creditCashAndPoint(MjonPayVO payVO, String userId) throws Exception {
|
|
| 330 |
+ |
|
| 331 |
+ // 공급가액 = 결제금액 / 11 * 10 (부가세 제외) |
|
| 332 |
+ long supply = Math.round(Double.parseDouble(payVO.getAmt()) / 11.0 * 10); |
|
| 333 |
+ String methodKo = payMethodKo(payVO.getPayMethod()); |
|
| 334 |
+ |
|
| 335 |
+ // --- 캐시 적립 (mj_cash + 회원 잔액 갱신) --- |
|
| 336 |
+ payVO.setUserId(userId); |
|
| 337 |
+ payVO.setFrstRegisterId(userId); |
|
| 338 |
+ payVO.setCash(supply); |
|
| 339 |
+ payVO.setOrderId(payVO.getMoid()); // 주문번호 |
|
| 340 |
+ payVO.setMsgGroupId(null); // 충전이므로 발송그룹 없음 |
|
| 341 |
+ payVO.setMemo(methodKo + " " + supply + " 충전"); |
|
| 342 |
+ // insertCash() 내부에서 cashId 생성 + insertCash + updateMemberCash 수행 |
|
| 343 |
+ mjonPayService.insertCash(payVO); |
|
| 344 |
+ |
|
| 345 |
+ // --- 포인트 적립 (문자 발송단가 미할인 회원만) --- |
|
| 346 |
+ int isMsgSalePrice = mjonPayDAO.selectMsgSalePriceCnt(userId); |
|
| 347 |
+ if (isMsgSalePrice == 0) {
|
|
| 348 |
+ JoinSettingVO sysJoinSetVO = mjonMsgDataService.selectJoinSettingInfo(); |
|
| 349 |
+ float pointPer = (sysJoinSetVO != null) ? sysJoinSetVO.getPointPer() : 0; |
|
| 350 |
+ int point = Math.round(supply * pointPer / 100f); |
|
| 351 |
+ |
|
| 352 |
+ payVO.setPointId(idgenMjonPointId.getNextStringId()); |
|
| 353 |
+ payVO.setPoint(point); |
|
| 354 |
+ payVO.setPointMemo(methodKo + " " + point + " 충전"); |
|
| 355 |
+ mjonPayDAO.insertPoint(payVO); // mj_point |
|
| 356 |
+ mjonPayDAO.updateMemberPoint(payVO); // 회원 포인트 갱신 |
|
| 357 |
+ } |
|
| 358 |
+ } |
|
| 359 |
+ |
|
| 360 |
+ /** 토스 결제수단 코드 → 캐시/포인트 메모용 한글명 */ |
|
| 361 |
+ private String payMethodKo(String payMethod) {
|
|
| 362 |
+ if ("CARD".equals(payMethod)) return "신용카드";
|
|
| 363 |
+ if ("SPAY".equals(payMethod)) return "간편결제";
|
|
| 364 |
+ if ("VBANK".equals(payMethod)) return "가상계좌";
|
|
| 365 |
+ if ("BANK".equals(payMethod)) return "계좌이체";
|
|
| 366 |
+ if ("CELLPHONE".equals(payMethod)) return "휴대폰";
|
|
| 367 |
+ return ""; |
|
| 368 |
+ } |
|
| 369 |
+ |
|
| 370 |
+ /** |
|
| 371 |
+ * 토스 결제조회 API (주문번호 기준) 호출. |
|
| 372 |
+ * GET https://api.tosspayments.com/v1/payments/orders/{orderId}
|
|
| 373 |
+ */ |
|
| 374 |
+ private JSONObject callTossPaymentByOrderId(String orderId) throws Exception {
|
|
| 375 |
+ String encodedKey = tossBasicAuth(); |
|
| 376 |
+ |
|
| 377 |
+ URL url = new URL("https://api.tosspayments.com/v1/payments/orders/" + orderId);
|
|
| 378 |
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
|
| 379 |
+ connection.setRequestMethod("GET");
|
|
| 380 |
+ connection.setRequestProperty("Authorization", "Basic " + encodedKey);
|
|
| 381 |
+ |
|
| 382 |
+ int responseCode = connection.getResponseCode(); |
|
| 383 |
+ InputStream responseStream = (responseCode == 200) ? connection.getInputStream() : connection.getErrorStream(); |
|
| 384 |
+ |
|
| 385 |
+ StringBuilder resBuilder = new StringBuilder(); |
|
| 386 |
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseStream, StandardCharsets.UTF_8))) {
|
|
| 387 |
+ String line; |
|
| 388 |
+ while ((line = reader.readLine()) != null) {
|
|
| 389 |
+ resBuilder.append(line); |
|
| 390 |
+ } |
|
| 391 |
+ } |
|
| 392 |
+ |
|
| 393 |
+ if (responseCode != 200) {
|
|
| 394 |
+ throw new Exception("토스 결제조회 실패(" + orderId + "): " + resBuilder.toString());
|
|
| 395 |
+ } |
|
| 396 |
+ return new JSONObject(resBuilder.toString()); |
|
| 397 |
+ } |
|
| 232 | 398 |
} |
--- src/main/java/itn/let/mjo/pay/web/MjonPayTossController.java
+++ src/main/java/itn/let/mjo/pay/web/MjonPayTossController.java
... | ... | @@ -17,11 +17,15 @@ |
| 17 | 17 |
import org.springframework.beans.factory.annotation.Value; |
| 18 | 18 |
import org.springframework.stereotype.Controller; |
| 19 | 19 |
import org.springframework.web.bind.annotation.RequestMapping; |
| 20 |
+import org.springframework.web.bind.annotation.ResponseBody; |
|
| 20 | 21 |
import org.springframework.web.servlet.mvc.support.RedirectAttributes; |
| 21 | 22 |
|
| 22 | 23 |
import javax.annotation.Resource; |
| 23 | 24 |
import javax.servlet.http.HttpServletRequest; |
| 25 |
+import java.io.BufferedReader; |
|
| 24 | 26 |
import java.util.Map; |
| 27 |
+ |
|
| 28 |
+import org.json.JSONObject; |
|
| 25 | 29 |
|
| 26 | 30 |
@Controller |
| 27 | 31 |
public class MjonPayTossController {
|
... | ... | @@ -127,4 +131,63 @@ |
| 127 | 131 |
|
| 128 | 132 |
return "redirect:/web/member/pay/PayView.do"; |
| 129 | 133 |
} |
| 134 |
+ |
|
| 135 |
+ /** |
|
| 136 |
+ * 토스페이먼츠 가상계좌 입금 콜백(웹훅). |
|
| 137 |
+ * 토스 서버 → 우리 서버로 호출되는 server-to-server 요청이므로 로그인 세션이 없다. |
|
| 138 |
+ * (context-security.xml 에서 /web/toss/webhook/** 는 security="none" 으로 허용) |
|
| 139 |
+ * |
|
| 140 |
+ * 콜백 본문(JSON)에서 orderId/status 만 추출한 뒤, 실제 입금완료 여부는 |
|
| 141 |
+ * 서비스에서 토스 결제조회 API로 재확인한 후 캐시/포인트를 적립한다. |
|
| 142 |
+ * 토스는 2xx 응답을 받지 못하면 재시도하므로, 처리 성공 시 항상 "OK"(200)를 반환한다. |
|
| 143 |
+ */ |
|
| 144 |
+ @RequestMapping(value = "/web/toss/webhook/deposit.do") |
|
| 145 |
+ @ResponseBody |
|
| 146 |
+ public String tossDepositWebhook(HttpServletRequest request, javax.servlet.http.HttpServletResponse response) {
|
|
| 147 |
+ |
|
| 148 |
+ try {
|
|
| 149 |
+ // 1. 요청 본문(JSON) 읽기 |
|
| 150 |
+ StringBuilder sb = new StringBuilder(); |
|
| 151 |
+ try (BufferedReader reader = request.getReader()) {
|
|
| 152 |
+ String line; |
|
| 153 |
+ while ((line = reader.readLine()) != null) {
|
|
| 154 |
+ sb.append(line); |
|
| 155 |
+ } |
|
| 156 |
+ } |
|
| 157 |
+ String raw = sb.toString(); |
|
| 158 |
+ LOGGER.info("토스 입금 콜백 수신: {}", raw);
|
|
| 159 |
+ |
|
| 160 |
+ if (raw.isEmpty()) {
|
|
| 161 |
+ return "OK"; |
|
| 162 |
+ } |
|
| 163 |
+ |
|
| 164 |
+ // 2. orderId / status 추출 (DEPOSIT_CALLBACK 평면 구조 / PAYMENT_STATUS_CHANGED data 구조 모두 대응) |
|
| 165 |
+ JSONObject body = new JSONObject(raw); |
|
| 166 |
+ String orderId; |
|
| 167 |
+ String status; |
|
| 168 |
+ if (body.has("data")) {
|
|
| 169 |
+ JSONObject data = body.getJSONObject("data");
|
|
| 170 |
+ orderId = data.optString("orderId");
|
|
| 171 |
+ status = data.optString("status");
|
|
| 172 |
+ } else {
|
|
| 173 |
+ orderId = body.optString("orderId");
|
|
| 174 |
+ status = body.optString("status");
|
|
| 175 |
+ } |
|
| 176 |
+ |
|
| 177 |
+ // 3. 입금완료(DONE) 콜백만 적립 처리 (그 외 상태는 무시하되 200 응답) |
|
| 178 |
+ if ("DONE".equals(status)) {
|
|
| 179 |
+ Map<String, Object> result = mjonPayTossService.processTossVbankDeposit(orderId); |
|
| 180 |
+ LOGGER.info("토스 입금 콜백 처리결과: {}", result);
|
|
| 181 |
+ } |
|
| 182 |
+ |
|
| 183 |
+ } catch (Exception e) {
|
|
| 184 |
+ // 처리 중 오류(네트워크/DB 등)는 500을 반환해 토스의 재시도를 유도한다. |
|
| 185 |
+ // processTossVbankDeposit 은 멱등하므로 재시도되어도 중복 적립되지 않는다. |
|
| 186 |
+ LOGGER.error("토스 입금 콜백 처리 오류", e);
|
|
| 187 |
+ response.setStatus(javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
|
| 188 |
+ return "FAIL"; |
|
| 189 |
+ } |
|
| 190 |
+ |
|
| 191 |
+ return "OK"; |
|
| 192 |
+ } |
|
| 130 | 193 |
} |
--- src/main/resources/egovframework/egovProps/globals_dev.properties
+++ src/main/resources/egovframework/egovProps/globals_dev.properties
... | ... | @@ -115,6 +115,11 @@ |
| 115 | 115 |
Globals.pay.kgm.mobile.mcSvcid=170622040674 |
| 116 | 116 |
Globals.pay.kgm.mobile.payMode=00 |
| 117 | 117 |
|
| 118 |
+#토스페이먼츠 설정 (시크릿키는 콜론 없이 키 본문만 저장) - 라이브(위젯) 키 |
|
| 119 |
+Globals.pay.toss.secretKey=live_gsk_DnyRpQWGrNbko96YwQk7rKwv1M9E |
|
| 120 |
+#토스 클라이언트키(공개키) - 프론트 위젯 초기화용 - 라이브(위젯) 키 |
|
| 121 |
+Globals.pay.toss.clientKey=live_gck_QbgMGZzorz59DZpopZElVl5E1em4 |
|
| 122 |
+ |
|
| 118 | 123 |
#Slack |
| 119 | 124 |
Globals.slack.hooks.url=https://hooks.slack.com/services/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 |
| 120 | 125 |
Globals.slack.channel.name=\ud14c\uc2a4\ud2b8_mjon\uba54\uc2dc\uc9c0 |
--- src/main/resources/egovframework/egovProps/globals_local.properties
+++ src/main/resources/egovframework/egovProps/globals_local.properties
... | ... | @@ -123,6 +123,11 @@ |
| 123 | 123 |
Globals.pay.kgm.mobile.mcSvcid=170622040674 |
| 124 | 124 |
Globals.pay.kgm.mobile.payMode=00 |
| 125 | 125 |
|
| 126 |
+#토스페이먼츠 설정 (시크릿키는 콜론 없이 키 본문만 저장) - 라이브(위젯) 키 |
|
| 127 |
+Globals.pay.toss.secretKey=live_gsk_DnyRpQWGrNbko96YwQk7rKwv1M9E |
|
| 128 |
+#토스 클라이언트키(공개키) - 프론트 위젯 초기화용 - 라이브(위젯) 키 |
|
| 129 |
+Globals.pay.toss.clientKey=live_gck_QbgMGZzorz59DZpopZElVl5E1em4 |
|
| 130 |
+ |
|
| 126 | 131 |
#Slack |
| 127 | 132 |
Globals.slack.hooks.url=https://hooks.slack.com/services/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 |
| 128 | 133 |
Globals.slack.channel.name=\ud14c\uc2a4\ud2b8_mjon\uba54\uc2dc\uc9c0 |
--- src/main/resources/egovframework/egovProps/globals_prod.properties
+++ src/main/resources/egovframework/egovProps/globals_prod.properties
... | ... | @@ -103,6 +103,11 @@ |
| 103 | 103 |
Globals.pay.kgm.mobile.mcSvcid=220613125202 |
| 104 | 104 |
Globals.pay.kgm.mobile.payMode=10 |
| 105 | 105 |
|
| 106 |
+#토스페이먼츠 설정 (시크릿키는 콜론 없이 키 본문만 저장) - 라이브(위젯) 키 |
|
| 107 |
+Globals.pay.toss.secretKey=live_gsk_DnyRpQWGrNbko96YwQk7rKwv1M9E |
|
| 108 |
+#토스 클라이언트키(공개키) - 프론트 위젯 초기화용 - 라이브(위젯) 키 |
|
| 109 |
+Globals.pay.toss.clientKey=live_gck_QbgMGZzorz59DZpopZElVl5E1em4 |
|
| 110 |
+ |
|
| 106 | 111 |
#Slack |
| 107 | 112 |
Globals.slack.hooks.url=https://hooks.slack.com/services/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 |
| 108 | 113 |
Globals.slack.channel.name=mjon\uba54\uc2dc\uc9c0 |
--- src/main/resources/egovframework/spring/com/context-security.xml
+++ src/main/resources/egovframework/spring/com/context-security.xml
... | ... | @@ -11,6 +11,8 @@ |
| 11 | 11 |
<security:http pattern="/images/**" security="none"/> |
| 12 | 12 |
<security:http pattern="/js/**" security="none"/> |
| 13 | 13 |
<security:http pattern="/resource/**" security="none"/> |
| 14 |
+ <!-- 토스페이먼츠 가상계좌 입금 콜백(웹훅) - 토스 서버에서 호출하는 server-to-server 요청이라 인증 제외 --> |
|
| 15 |
+ <security:http pattern="/web/toss/webhook/**" security="none"/> |
|
| 14 | 16 |
<security:http pattern="\A/WEB-INF/jsp/.*\Z" request-matcher="regex" security="none"/> |
| 15 | 17 |
|
| 16 | 18 |
<egov-security:config id="securityConfig" |
--- src/main/resources/egovframework/sqlmap/let/pay/MjonPay_SQL_mysql.xml
+++ src/main/resources/egovframework/sqlmap/let/pay/MjonPay_SQL_mysql.xml
... | ... | @@ -1456,9 +1456,19 @@ |
| 1456 | 1456 |
, CANCEL_TIME = DATE(NOW()) |
| 1457 | 1457 |
</isEqual> |
| 1458 | 1458 |
WHERE MOID = #moid# |
| 1459 |
- |
|
| 1459 |
+ |
|
| 1460 | 1460 |
</update> |
| 1461 |
- |
|
| 1461 |
+ |
|
| 1462 |
+ <!-- 토스 가상계좌 입금완료 처리 (PG_STATUS 0:입금대기 → 1:결제완료). 멱등 처리를 위해 0인 건만 갱신 --> |
|
| 1463 |
+ <update id="mjonPayDAO.updateTossVbankDeposit" parameterClass="mjonPayVO"> |
|
| 1464 |
+ UPDATE MJ_PG |
|
| 1465 |
+ SET PG_STATUS = '1', |
|
| 1466 |
+ RESULT_CODE = '0000', |
|
| 1467 |
+ RESULT_MSG = '입금이 정상 처리되었습니다.' |
|
| 1468 |
+ WHERE MOID = #moid# |
|
| 1469 |
+ AND PG_STATUS = '0' |
|
| 1470 |
+ </update> |
|
| 1471 |
+ |
|
| 1462 | 1472 |
<select id="mjonPayDAO.selectPayDayChartDashboard" parameterClass="mjonPayVO" resultClass="mjonPayVO"> |
| 1463 | 1473 |
|
| 1464 | 1474 |
SELECT |
--- src/main/webapp/WEB-INF/jsp/web/pay/PayView.jsp
+++ src/main/webapp/WEB-INF/jsp/web/pay/PayView.jsp
... | ... | @@ -6,6 +6,9 @@ |
| 6 | 6 |
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> |
| 7 | 7 |
<%@ taglib prefix="ec" uri="/WEB-INF/tld/ecnet_tld.tld"%> |
| 8 | 8 |
|
| 9 |
+<%-- 토스 클라이언트키(공개키)는 환경별 globals_*.properties 에서 주입 --%> |
|
| 10 |
+<spring:eval expression="@globalSettings['Globals.pay.toss.clientKey']" var="tossClientKey"/> |
|
| 11 |
+ |
|
| 9 | 12 |
<style> |
| 10 | 13 |
/*.charg_cont .area_tab li{ width: calc((100% - 80px)/5);}*/
|
| 11 | 14 |
</style> |
... | ... | @@ -30,39 +33,101 @@ |
| 30 | 33 |
alert("결제에 실패하였습니다.\n사유: " + errorMsg);
|
| 31 | 34 |
} |
| 32 | 35 |
|
| 33 |
- |
|
| 34 |
- setPriceMake(); |
|
| 35 |
- // 다음 결제시 결제수단 SELECT (필요 시 유지) |
|
| 36 |
- // getNextPayMethod(); |
|
| 37 |
- |
|
| 38 |
- // 토스페이먼츠 위젯 초기화 세팅 |
|
| 39 |
- const clientKey = 'test_gck_KNbdOvk5rk15kdKpqQGo3n07xlzm'; |
|
| 36 |
+ // 토스페이먼츠 위젯 초기화 세팅 (clientKey는 서버 globals_*.properties 에서 주입) |
|
| 37 |
+ const clientKey = '${tossClientKey}';
|
|
| 40 | 38 |
const customerKey = 'test_customer_1234'; // 테스트용 가상 고객 ID (실제 연동 시에는 로그인한 유저의 고유 ID를 넣으세요) |
| 41 | 39 |
paymentWidget = PaymentWidget(clientKey, customerKey); |
| 42 | 40 |
|
| 43 |
- // 충전금액 세팅 및 렌더링 |
|
| 44 |
- setPriceMake(); |
|
| 45 |
- const initialAmount = parseInt($("#price").val(), 10);
|
|
| 41 |
+ // 초기 충전금액(입력값 기준, 공급가액) |
|
| 42 |
+ var initSupply = parseInt(($("#price").val() || '0').replace(/[^0-9]/g, ''), 10) || 0;
|
|
| 43 |
+ var initAmount = initSupply + Math.round(initSupply * 0.1); |
|
| 46 | 44 |
|
| 47 | 45 |
// 결제수단 및 약관 위젯 화면에 렌더링 |
| 48 |
- paymentMethodWidget = paymentWidget.renderPaymentMethods('#payment-method', { value: initialAmount });
|
|
| 46 |
+ paymentMethodWidget = paymentWidget.renderPaymentMethods('#payment-method', { value: initAmount });
|
|
| 49 | 47 |
paymentWidget.renderAgreement('#agreement');
|
| 48 |
+ |
|
| 49 |
+ // 선택한 결제수단 → 아래 신용카드 타이틀 텍스트 동기화 |
|
| 50 |
+ try {
|
|
| 51 |
+ if (paymentMethodWidget && typeof paymentMethodWidget.on === 'function') {
|
|
| 52 |
+ // 위젯 렌더 완료 후 초기 반영 + 선택 변경 감지(폴링: v1은 select 이벤트 미보장) |
|
| 53 |
+ paymentMethodWidget.on('ready', function () {
|
|
| 54 |
+ updatePayMethodTitle(); |
|
| 55 |
+ setInterval(updatePayMethodTitle, 100); |
|
| 56 |
+ }); |
|
| 57 |
+ // select 이벤트가 지원되면 즉시 반영 |
|
| 58 |
+ paymentMethodWidget.on('paymentMethodSelect', updatePayMethodTitle);
|
|
| 59 |
+ } |
|
| 60 |
+ } catch (e) { /* 이벤트 미지원 환경 무시 */ }
|
|
| 61 |
+ |
|
| 62 |
+ // 초기 금액 표시 갱신 |
|
| 63 |
+ updatePrice(initSupply); |
|
| 64 |
+ |
|
| 65 |
+ // 금액 추가 버튼 클릭 |
|
| 66 |
+ $('.add_money .btn').on('click', function () {
|
|
| 67 |
+ var addAmount = Number($(this).data('price'));
|
|
| 68 |
+ var currentValue = $("#price").val().replace(/[^0-9]/g, '');
|
|
| 69 |
+ currentValue = currentValue ? Number(currentValue) : 0; |
|
| 70 |
+ var total = currentValue + addAmount; |
|
| 71 |
+ $("#price").val(numberWithCommas(total));
|
|
| 72 |
+ updatePrice(total); |
|
| 73 |
+ }); |
|
| 74 |
+ |
|
| 75 |
+ // 직접 입력 |
|
| 76 |
+ $("#price").on('input', function () {
|
|
| 77 |
+ var value = $(this).val().replace(/[^0-9]/g, ''); |
|
| 78 |
+ value = value ? Number(value) : 0; |
|
| 79 |
+ $(this).val(numberWithCommas(value)); |
|
| 80 |
+ updatePrice(value); |
|
| 81 |
+ }); |
|
| 50 | 82 |
}); |
| 51 | 83 |
|
| 52 |
- function setPriceMake() {
|
|
| 53 |
- var tempPrice = parseInt($('.list_seType1').val(), 10);
|
|
| 54 |
- var vatPrice = Math.round(parseInt(tempPrice, 10) * 0.1); |
|
| 55 |
- var lastPrice = parseInt(tempPrice, 10) + parseInt(vatPrice, 10); |
|
| 84 |
+ // 금액 업데이트 (price = 공급가액) |
|
| 85 |
+ function updatePrice(price) {
|
|
| 86 |
+ var $message = $('.input_message');
|
|
| 56 | 87 |
|
| 57 |
- $("#price").val(lastPrice);
|
|
| 58 |
- $('#supplyPriceStr').html(numberWithCommas(tempPrice));
|
|
| 59 |
- $('#vatPriceStr').html(numberWithCommas(vatPrice));
|
|
| 60 |
- $('#lastPriceStr').html(numberWithCommas(lastPrice));
|
|
| 88 |
+ var supplyPrice = price; |
|
| 89 |
+ var vatPrice = Math.round(supplyPrice * 0.1); |
|
| 90 |
+ var lastPrice = supplyPrice + vatPrice; |
|
| 61 | 91 |
|
| 62 |
- // [추가] 변경된 최종 결제 금액을 토스 위젯에 업데이트 |
|
| 92 |
+ // 화면 표시 |
|
| 93 |
+ $('#supplyPriceStr').text(numberWithCommas(supplyPrice));
|
|
| 94 |
+ $('#vatPriceStr').text(numberWithCommas(vatPrice));
|
|
| 95 |
+ $('#lastPriceStr').text(numberWithCommas(lastPrice));
|
|
| 96 |
+ |
|
| 97 |
+ // 토스 결제 금액 변경 |
|
| 63 | 98 |
if (typeof paymentMethodWidget !== 'undefined') {
|
| 64 | 99 |
paymentMethodWidget.updateAmount(lastPrice); |
| 65 | 100 |
} |
| 101 |
+ |
|
| 102 |
+ // 충전금액 제한 체크 |
|
| 103 |
+ if (price > 150000 || price < 5000) {
|
|
| 104 |
+ $(".money_input").addClass('error');
|
|
| 105 |
+ $message.addClass('active');
|
|
| 106 |
+ if (price < 5001) {
|
|
| 107 |
+ $message.find(".msg").text("최소 충전금액 5천원 이상 입력해주세요.");
|
|
| 108 |
+ } else {
|
|
| 109 |
+ $message.find(".msg").text("휴대폰 결제는 최대 150,000원까지 입력 가능합니다.");
|
|
| 110 |
+ } |
|
| 111 |
+ } else {
|
|
| 112 |
+ $(".money_input").removeClass('error');
|
|
| 113 |
+ $message.removeClass('active');
|
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ // 선택된 결제수단명을 신용카드 타이틀에 반영 |
|
| 118 |
+ function updatePayMethodTitle() {
|
|
| 119 |
+ try {
|
|
| 120 |
+ var sel = paymentMethodWidget.getSelectedPaymentMethod(); |
|
| 121 |
+ if (!sel || !sel.method) return; |
|
| 122 |
+ |
|
| 123 |
+ var method = sel.method; // 카드 / 가상계좌 / 계좌이체 / 휴대폰 / 간편결제 ... |
|
| 124 |
+ var label = (method === '카드') ? '신용·체크카드' : method; |
|
| 125 |
+ |
|
| 126 |
+ var $t = $('#payMethodTitle');
|
|
| 127 |
+ if ($t.length && $t.text() !== label) {
|
|
| 128 |
+ $t.text(label); |
|
| 129 |
+ } |
|
| 130 |
+ } catch (e) { /* 위젯 준비 전 호출 무시 */ }
|
|
| 66 | 131 |
} |
| 67 | 132 |
|
| 68 | 133 |
// 후불제여부 체크 |
... | ... | @@ -97,28 +162,24 @@ |
| 97 | 162 |
return false; |
| 98 | 163 |
} |
| 99 | 164 |
|
| 100 |
- var lastPrice = parseInt($("#price").val(), 10);
|
|
| 101 |
- if(lastPrice < 5500){
|
|
| 102 |
- alert("최소 충전금액 5천원 이상 선택해주세요.");
|
|
| 165 |
+ // 충전금액(공급가액) 추출 |
|
| 166 |
+ var chargePrice = parseInt(($("#price").val() || '0').replace(/[^0-9]/g, ''), 10);
|
|
| 167 |
+ if (isNaN(chargePrice) || chargePrice < 5000) {
|
|
| 168 |
+ alert("최소 충전금액 5천원 이상 입력해주세요.");
|
|
| 103 | 169 |
return false; |
| 104 | 170 |
} |
| 105 |
- // const paymentMethodsWidget = paymentWidget.renderPaymentMethods(); |
|
| 106 |
- // const selectedPaymentMethod = paymentMethodsWidget.getSelectedPaymentMethod(); |
|
| 107 |
- // console.log('paymentMethodsWidget : ', paymentMethodsWidget);
|
|
| 108 |
- // console.log('selectedPaymentMethod : ', selectedPaymentMethod);
|
|
| 171 |
+ |
|
| 172 |
+ // 최종 결제금액(부가세 포함) |
|
| 173 |
+ var lastPrice = chargePrice + Math.round(chargePrice * 0.1); |
|
| 109 | 174 |
|
| 110 | 175 |
// 토스페이먼츠 결제창 바로 호출 |
| 111 | 176 |
paymentWidget.requestPayment({
|
| 112 | 177 |
orderId: 'ORDER_' + new Date().getTime(), // 고유한 상점 주문번호 생성 |
| 113 | 178 |
orderName: '문자온 충전 ' + lastPrice + '원', |
| 114 |
- successUrl: window.location.origin + '/web/member/pay/tossSuccess.do', // 결제 성공 시 이동할 URL (백엔드 컨트롤러 생성 필요) |
|
| 115 |
- // paymentType=NORMAL |
|
| 116 |
- // &orderId=ORDER_1778047452906 |
|
| 117 |
- // &paymentKey=tmunj2026050615041659Zv9 |
|
| 118 |
- // &amount=55000 |
|
| 119 |
- failUrl: window.location.origin + '/web/member/pay/tossFail.do', // 결제 실패 시 이동할 URL (백엔드 컨트롤러 생성 필요) |
|
| 120 |
- customerEmail: $('#mberEmailAdres').val(), // 필요시 로그인한 사용자의 이메일 매핑
|
|
| 121 |
- customerName: $('#mberNm').val(), // 필요시 로그인한 사용자의 이름 매핑
|
|
| 179 |
+ successUrl: window.location.origin + '/web/member/pay/tossSuccess.do', // 결제 성공 시 이동할 URL |
|
| 180 |
+ failUrl: window.location.origin + '/web/member/pay/tossFail.do', // 결제 실패 시 이동할 URL |
|
| 181 |
+ customerEmail: $('#mberEmailAdres').val(), // 로그인한 사용자의 이메일 매핑
|
|
| 182 |
+ customerName: $('#mberNm').val(), // 로그인한 사용자의 이름 매핑
|
|
| 122 | 183 |
customerMobilePhone: $('#moblphonNo').val()
|
| 123 | 184 |
}).catch(function (error) {
|
| 124 | 185 |
if (error.code === 'USER_CANCEL') {
|
... | ... | @@ -128,13 +189,6 @@ |
| 128 | 189 |
} |
| 129 | 190 |
}); |
| 130 | 191 |
} |
| 131 |
- |
|
| 132 |
- //충전금액 Change Event |
|
| 133 |
- $(document).on('change', '#tempPrice', function() {
|
|
| 134 |
- // $(document).on('change', '.list_seType1', function() {
|
|
| 135 |
- // 충전금액 세팅 |
|
| 136 |
- setPriceMake(); |
|
| 137 |
- }); |
|
| 138 | 192 |
|
| 139 | 193 |
|
| 140 | 194 |
/* 윈도우팝업 열기 */ |
... | ... | @@ -173,8 +227,7 @@ |
| 173 | 227 |
<form id="payTypeForm" name="payTypeForm" method="post"> |
| 174 | 228 |
<input type="hidden" name="payTypeCode" /> |
| 175 | 229 |
</form> |
| 176 |
- <form id="pgForm" name="pgForm" action="/web/member/pay/PayActionAjax.do" method="post"> |
|
| 177 |
- <input type="hidden" id="price" name="price" /> |
|
| 230 |
+ <form id="pgForm" name="pgForm" action="/web/member/pay/PayActionAjax.do" method="post"> |
|
| 178 | 231 |
<input type="hidden" id="payMethod" name="payMethod" /> |
| 179 | 232 |
<input type="hidden" id="accMsg" name="accMsg" /> |
| 180 | 233 |
<input type="hidden" id="sendCnt" name="sendCnt" value="<c:out value='${resultMsgInfo.sendCnt}'/>" />
|
... | ... | @@ -207,47 +260,60 @@ |
| 207 | 260 |
<p>- 모든 요금은 VAT별도 금액입니다.</p> |
| 208 | 261 |
</div>--%> |
| 209 | 262 |
<div> |
| 210 |
- <p class="tab_tit">충전수단 선택</p><%-- |
|
| 211 |
- <ul class="area_tab"> |
|
| 212 |
- <li class="btn_charge1 btn_tab active"><button type="button" onclick="TabTypePay(this,'1');"><i></i>신용카드</button></li> |
|
| 213 |
- <li class="btn_charge2 btn_tab"><button type="button" onclick="TabTypePay(this,'2');" id="btnDdedicatedAccount"><i></i>전용계좌</button></li> |
|
| 214 |
- <li class="btn_charge3 btn_tab"><button type="button" onclick="TabTypePay(this,'3');"><i></i>휴대폰결제</button></li> |
|
| 215 |
- <li class="btn_charge4 btn_tab"><button type="button" onclick="TabTypePay(this,'4');"><i></i>즉시이체</button></li> |
|
| 216 |
- |
|
| 217 |
- <li class="btn_charge5 btn_tab simple_pay"><button type="button" onclick="TabTypePay(this,'5');"><i></i></button></li> |
|
| 218 |
- <li class="btn_charge6 btn_tab simple_pay"><button type="button" onclick="TabTypePay(this,'6');"><i></i></button></li> |
|
| 219 |
- <li class="btn_charge7 btn_tab simple_pay"><button type="button" onclick="TabTypePay(this,'7');"><i></i></button></li> |
|
| 220 |
- <li class="btn_charge8 btn_tab simple_pay"><button type="button" onclick="TabTypePay(this,'8');"><i></i></button></li> |
|
| 221 |
- </ul> |
|
| 222 |
- <div class="checkbox_wrap"><input type="checkbox" id="agree"><label for="agree">선택한 수단을 다음 충전 시에도 이용합니다.</label></div> |
|
| 223 |
---%> |
|
| 263 |
+ <!-- 토스페이먼츠 결제수단 위젯 영역 --> |
|
| 264 |
+ <div id="payment-method" class="toss_wrap"></div> |
|
| 265 |
+ |
|
| 266 |
+ <!-- 토스페이먼츠 이용약관 영역 --> |
|
| 267 |
+ <div id="agreement" class="toss_wrap"></div> |
|
| 268 |
+ |
|
| 224 | 269 |
<!-- 신용카드 --> |
| 225 | 270 |
<div class="area_tabcont on" id="tab2_1"> |
| 226 |
- <p class="tType1_title"><img src="/publish/images/content/icon_charging1_small.png" alt=""> 신용카드</p> |
|
| 227 |
- <!-- 충전금액 선택 영역 --> |
|
| 228 |
- <div class="charge_amount_box" style="margin-bottom: 20px;"> |
|
| 229 |
- <label for="tempPrice" style="font-weight: bold; margin-right: 10px;">충전금액 선택 :</label> |
|
| 230 |
- <select name="tempPrice" id="tempPrice" class="list_seType1"> |
|
| 231 |
- <option value="5000">5,000</option> |
|
| 232 |
- <option value="10000">10,000</option> |
|
| 233 |
- <option value="50000" selected>50,000</option> |
|
| 234 |
- <!-- 필요하신 금액대 옵션 유지 --> |
|
| 235 |
- </select> 원 |
|
| 236 |
- <p style="margin-top: 10px; color: #666;"> |
|
| 237 |
- 최종 결제금액: <strong id="lastPriceStr" style="color: #000; font-size: 16px;">55,000</strong>원 (공급가액 <span id="supplyPriceStr">50,000</span>원 + 부가세 <span id="vatPriceStr">5,000</span>원) |
|
| 238 |
- </p> |
|
| 239 |
- </div> |
|
| 271 |
+ <p class="tType1_title"><img src="/publish/images/credit_small.png" alt="신용카드"> <span id="payMethodTitle">신용·체크카드</span></p> |
|
| 272 |
+ <table class="tType1"> |
|
| 273 |
+ <colgroup> |
|
| 274 |
+ <col style="width: 100px;"> |
|
| 275 |
+ <col style="width: auto;"> |
|
| 276 |
+ </colgroup> |
|
| 277 |
+ <tbody> |
|
| 278 |
+ <tr class="charge_content"> |
|
| 279 |
+ <th scope="row">충전금액</th> |
|
| 280 |
+ <td class="flex"> |
|
| 281 |
+ <div class="money_form_wrap form_wrap"> |
|
| 282 |
+ <input type="text" name="price" id="price" class="input money_input" placeholder="5,000원 이상 입력해주세요." value="50,000" /> |
|
| 283 |
+ <p class="input_in">원</p> |
|
| 284 |
+ <p class="input_message error"> |
|
| 285 |
+ <i class="icon error">!</i> <span class="msg">휴대폰 결제는 최대 150,000원까지 입력 가능합니다.</span> |
|
| 286 |
+ </p> |
|
| 287 |
+ </div> |
|
| 240 | 288 |
|
| 241 |
- <!-- 토스페이먼츠 결제수단 위젯 영역 --> |
|
| 242 |
- <div id="payment-method"></div> |
|
| 289 |
+ <div class="btn_wrap add_money w100per"> |
|
| 290 |
+ <button type="button" class="btn" data-price="10000">+ 1만원</button> |
|
| 291 |
+ <button type="button" class="btn" data-price="50000">+ 5만원</button> |
|
| 292 |
+ <button type="button" class="btn" data-price="100000">+ 10만원</button> |
|
| 293 |
+ <button type="button" class="btn" data-price="1000000">+ 100만원</button> |
|
| 294 |
+ </div> |
|
| 295 |
+ </td> |
|
| 296 |
+ </tr> |
|
| 243 | 297 |
|
| 244 |
- <!-- 토스페이먼츠 이용약관 영역 --> |
|
| 245 |
- <div id="agreement"></div> |
|
| 246 |
- |
|
| 247 |
- <!-- 결제하기 버튼 --> |
|
| 248 |
- <div style="text-align: center; margin-top: 20px;"> |
|
| 249 |
- <button type="button" class="btnType" onclick="pgOpenerPopup(); return false;" style="width: 200px; height: 50px; font-size: 16px;">충전하기</button> |
|
| 250 |
- </div> |
|
| 298 |
+ <tr> |
|
| 299 |
+ <td colspan="2"> |
|
| 300 |
+ <div class="amount_wrap"> |
|
| 301 |
+ <dl> |
|
| 302 |
+ <dt>최종 결제금액 :</dt> |
|
| 303 |
+ <dd> |
|
| 304 |
+ <ul> |
|
| 305 |
+ <li><strong id="supplyPriceStr">50,000</strong>원(공급가액)</li> |
|
| 306 |
+ <li><span class="plus"></span><strong id="vatPriceStr">5,000</strong>원(부가세)</li> |
|
| 307 |
+ <li class="total"><span class="equal"></span><strong class="c_e40000" id="lastPriceStr">55,000</strong>원(최종금액)</li> |
|
| 308 |
+ </ul> |
|
| 309 |
+ </dd> |
|
| 310 |
+ </dl> |
|
| 311 |
+ <button type="button" class="btn btnType fill primary btn_pay" onclick="pgOpenerPopup(); return false;">충전하기</button> |
|
| 312 |
+ </div> |
|
| 313 |
+ </td> |
|
| 314 |
+ </tr> |
|
| 315 |
+ </tbody> |
|
| 316 |
+ </table> |
|
| 251 | 317 |
</div> |
| 252 | 318 |
</div> |
| 253 | 319 |
|
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?