티스토리 뷰

728x90

공식문서도 NodeJS로 되어있어 다른 자료를 참고했다.

연동 자체는 간단한데 외부 API를 처음 연동해보는거라 여러가지 실수했던 곳이 많아 미래의 실수를 막기 위해 작성한다.

 

https://sumin2.tistory.com/58

 

스프링부트+jsp로 배달사이트 만들기-40 결제api 사용해서 주문하기(아임포트)

https://www.iamport.kr/ 온라인 비즈니스의 모든 결제를 한곳에서, 아임포트 결제의 시작부터 비즈니스의 성장까지 아임포트와 함께하세요 www.iamport.kr 아임포트에 회원가입을 하고 관리자 콘솔로 이

sumin2.tistory.com

 

위 사이트를 참고했으며 본인은 바닐라 자바스크립트를 이용했다.

 

자바스크립트

 

 

아임포트 Docs에도 나와있듯이 결제 전에 미리 결제 번호를 부여하고 저장해두는 것을 권고하고 있기에

본인은 그 과정을 API로 보내 작성 후 DB에 저장하고자

onmousedown="paymentRecord();paymentRecord();"와 같은 형식으로 작성했었다.

하지만 이 방법은 함수의 순서를 확실하게 결정해주지 않았다. 비동기 동기의 문제인걸로 생각되었고 순서를 확실하게 정해주기 위해 

선결되어야 하는 정보를 onmousedown으로 설정해 미리 실행될 수 있도록 하였음.

<button name="paymentButton" id="paymentButton" onmousedown="paymentRecord();" onclick="requestPay();" class="w-100 btn btn-warning btn-lg"
type="submit">결제하기</button>

 

paymentRecord() 함수는 다음과 같다.

서버에서 정보를 받아와야 하므로 변수를 외부에 선언해주고 HTML에서 값을 받아와 서버로 올려 레코드 저장 이후 

서버에서 받아온 총액(amount)과 상품번호(merchantUid)를 외부 변수에 저장해두었다.

 

위에서 말한 순서의 문제가 여기서 발생했는데 onclick에 함수 2개를 같이 묶어두면 서버에서 받아온 정보가 변수에 매핑되지 않는 문제가 생겼었음.

 

그리고 실패하는 구간마다 preventDefault()를 걸어 실제 결제 창으로 넘어가지 않도록 하였다.

            <script>
                var IMP = window.IMP;

                let pg;
                let payMethod;
                let buyerEmail;
                let buyerName;
                let buyerTel;
                let buyerAddr;
                let buyerPostcode;
                let amount;
                let merchantUid;
                let name = document.getElementById('name').textContent;
                function paymentRecord() {
                    pg = document.getElementById('pg')[document.getElementById('pg')
                        .selectedIndex].value.toUpperCase();
                    if (pg == '결제 방법 선택') {
                        alert('결제 방법을 선택해주세요');
                        preventDefault();
                    }
                    payMethod = document.getElementById('payMethod')[document.getElementById('payMethod')
                        .selectedIndex].value.toUpperCase();
                    if (payMethod == '결제 수단 선택') {
                        alert('결제 수단을 선택해주세요');
                        preventDefault();
                    }
                    buyerEmail = document.getElementById('buyerEmail').value;
                    buyerName = document.getElementById('buyerName').value;
                    buyerTel = document.getElementById('buyerTel').value;
                    buyerAddr = document.getElementById('buyerAddr').value;
                    buyerPostcode = document.getElementById('buyerPostcode').value;

                    let data = {
                        method: 'POST',
                        body: JSON.stringify({
                            pg,
                            payMethod,
                            buyerEmail,
                            buyerName,
                            buyerTel,
                            buyerAddr,
                            buyerPostcode
                        }),
                        headers: {
                            'Content-Type': 'application/json',
                        }
                    };
                    const onSuccess = res => {
                        res.json()
                            .then((json) => {
                                amount = json.amount;
                                merchantUid = json.merchantUid;
                            })
                    }
                    const onFailure = res => {
                        return res.json()
                            .then(json => {
                                alert(json.message);
                                preventDefault();
                            })
                    }

                    fetch(`/api/v1/payment/record`, data)
                        .then(res => {
                            if (!res.ok) {
                                throw res;
                            }
                            return res
                        })
                        .then(onSuccess, onFailure)
                        .catch(err => {
                            console.log(err.message);
                        });
                };

 

그리고 실제로 아임포트 서버에 결제 요청을 하는 과정은 다음과 같다.

 

위에 전역 변수로 설정해두었던 값들을 parameter에 넣는다.

이때 주의해야 할 점은 상품 이름인 name은 필수인 pg사가 있으므로 반드시 작성하는 것이 좋다.

본인은 name 생성 전략을 생각하지 못해 그냥 생략하였다가 에러를 맛봄

그리고 m_redirect_url: ' '; 를 설정해두어야 결제가 완료되고 리다이렉트가 가능하다.

없어도 무방.

 

ajax로 서버에 요청하는 url은 결제가 완료된 정보를 받는 곳이다. 이곳에서 토큰을 생성해서 유효성 검사를 해야한다.

 

function requestPay() {
                    
                    IMP.init("본인 번호 작성");
                    // IMP.request_pay(param, callback) 결제창 호출

                    IMP.request_pay({ // param
                        pg: pg,
                        pay_method: payMethod,
                        merchant_uid: merchantUid,
                        amount: amount,
                        name: name,
                        buyer_email: buyerEmail,
                        buyer_name: buyerName,
                        buyer_tel: buyerTel,
                        buyer_addr: buyerAddr,
                        buyer_postcode: buyerPostcode,
                        m_redirect_url: "/order/payment-result"

                    }, function (rsp) { // callback
                        if (rsp.success) {

                            jQuery.ajax({
                                url: "/api/v1/payment/complete", // 예: https://www.myservice.com/payments/complete
                                method: "POST",
                                headers: { "Content-Type": "application/json" },
                                data: {
                                    imp_uid: rsp.imp_uid,
                                    merchant_uid: rsp.merchant_uid
                                }
                            }).done(function (data) {
                                location.replace('/order/payment-result');
                            })
                        } else {
                            alert("결제에 실패하였습니다. 에러 내용: " + rsp.error_msg);
                        }
                    });
                }

 

 

스프링

 

컨트롤러

 

레코드를 생성하는 것은 본인의 자유이므로 사용하든 사용하지 않든 상관없지만

컨트롤러에서 가장 주의해야 할 점은 imp_uid를 @RequestBody로 받는 것이다.

정확히는 imp_uid=imp_211711115114&merchant_uid=ORD20220601-182900287475800

처럼 파라미터 형식으로 오지만 JSON과 String의 형식으로 받아야한다.

 

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/payment")
public class PaymentApiController {

    private final PaymentService paymentService;

    @PostMapping("/record")
    public ResponseEntity<?> paymentRecordGenerateBeforePg(@Valid @RequestBody PaymentRequest paymentRequest, @CurrentUser User user) {
        PaymentResponse paymentResponse = paymentService.paymentRecordGenerateBeforePg(paymentRequest, user);

        return ResponseEntity.ok(paymentResponse);
    }

    @PostMapping("/complete")
    public ResponseEntity<?> paymentResult(@RequestBody String imp_uid) throws JSONException, IOException {
        System.out.println("imp_uid : " + imp_uid);

        String token = paymentService.getToken();
        System.out.println("token : " + token);

        return ResponseEntity.ok().build();
    }
}

 

 

서비스

 

방법을 여러가지지만 JSON의 파싱을 위해 GSON을 build.gradle에 넣어야한다.

implementation group: 'com.google.code.gson', name: 'gson', version: '2.7'

 

이후 application.properties에 다음과 같이 key, value를 세팅한다. secret이나 password는 반드시 ignore되는 yml이나 properties에 보관해야 한다.

 

보안을 위해 많이 잘라내었다. 이것보다 훨씬 길 것이다.

imp_key=8753437
imp_secret=fec4af82c2d97e8fa4ebdb2c6a8ffb5d021b5794

 

여기서 사용될 라이브러리는 딱 이것뿐이다. 이것때문에 좀 많이 헷갈렸음.

import com.google.gson.Gson;
import com.google.gson.JsonObject;

 

그 다음 key, secret을 가져오고

@Value("${imp_key}")
private String imp_key;

@Value("${imp_secret}")
private String imp_secret;

 

 

실제로 결제 정보의 유효성을 확인하기 위한 token을 생성하는 과정이다.

 

밑의 내용은 한 톨도 빠지지 않고 그냥 복붙하면 된다.

 

public String getToken() throws IOException {

    HttpsURLConnection conn = null;

    URL url = new URL("https://api.iamport.kr/users/getToken");

    conn = (HttpsURLConnection) url.openConnection();

    conn.setRequestMethod("POST");
    conn.setRequestProperty("Content-type", "application/json");
    conn.setRequestProperty("Accept", "application/json");
    conn.setDoOutput(true);
    JsonObject json = new JsonObject();

    json.addProperty("imp_key", imp_key);
    json.addProperty("imp_secret", imp_secret);

    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));

    bw.write(json.toString());
    bw.flush();
    bw.close();

    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));

    Gson gson = new Gson();

    String response = gson.fromJson(br.readLine(), Map.class).get("response").toString();

    System.out.println("response : " + response);

    String token = gson.fromJson(response, Map.class).get("access_token").toString();

    br.close();
    conn.disconnect();

    return token;
}

 

여기까지 진행하면 실제로 결제가 되고 결제 완료까지 창이 redirect되는 것을 확인할 수 있다.

하지만 유효성 검사를 진행하지 않았으므로 레코드와 비교대조를 통해 검사를 진행하여야 한다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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
글 보관함