장고 - 결제 시스템 연동

  • 이번 포스트에서는 온라인 쇼핑몰 사이트를 예로 들어, 결제 시스템을 어떻게 연동하는지에 대해 알아볼 것이다.
  • 여기에서는 iamport를 사용하여 결제 시스템을 연동할 것이다.

iamport 결제 관리 시스템 이용

1. iamport 사이트 접속

  • https://www.iamport.kr
  • 우측 상단 ‘대사보드’ 클릭
  • 회원가입 및 로그인
  • 시스템설정 > PG설정(일반결제 및 정기결제)
    • PG사 : KG이니시스(웹표준결제창) 선택
    • 테스트모드 : ON

2. 장고 프로젝트에 IAMPORT 관련 정보 설정

  • 경로 : config(프로젝트) > settings.py
    1
    2
    3
    4
    ...
    # iamport 사이트 > 시스템설정 > 내정보에서 IAMPORT_KEY, IAMPORT_SECRET 정보 입력
    IAMPORT_KEY = 'REST API 키'
    IAMPORT_SECRET = 'REST API secret'

3. 장바구니 페이지에서 결제 버튼 클릭 시, 결제페이지로 이동되도록 코드 작성

  • 경로 : cart > templates > cart > cart.detail.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ...
    <!-- 클릭 시, 결제페이지로 이동되는 버튼 부분 -->
    <tr>
    <td colspan="7">
    <!-- 이전 페이지 이동 -->
    <a href="{{continue_url}}" class="float-left btn btn-lg btn-primary">Continue Shopping</a>
    <!-- 결제페이지로 이동 -->
    <a href="{% url 'order_create' %}" class="float-right btn btn-lg btn-info">Checkout</a>
    </td>
    </tr>
    {% endblock %}

4. 결제 페이지로 이동할 경로 작성

  • 경로 : order > urls.py
    1
    2
    3
    4
    5
    6
    from django.urls import path

    from .views import *
    urlpatterns = [
    path('create/', order_create, name='order_create'),
    ]

5. 결제 페이지 코드 작성

  • 경로 : order > views.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    from django.shortcuts import render

    from cart.cart import Cart
    from .models import OrderItem
    from .forms import OrderForm

    def order_create(request):
    # 장바구니에 존재하는 상품 정보 받아오기
    cart = Cart(request)
    # 주문 정보가 입력 완료된 상황(결제 폼 작성 완료하여 POST 형태로 request를 받은 상황)
    if request.method == "POST":
    form = OrderForm(request.POST)
    # 작성한 폼 validation 진행
    if form.is_valid():
    order = form.save()
    for item in cart:
    # 장바구니에 들어있는 모든 상품 정보를 OrderItem 모델에 저장
    OrderItem.objects.create(order=order, product=item['product'], price=item['price'], quantity=item['quantity'])
    return render(request, 'order/order_created.html', {'order': order})
    # 주문 정보가 입력되지 않은 상황(처음 결제페이지로 이동한 상황)
    else:
    form = OrderForm()

    return render(request, 'order/order_create.html', {'form':form})
  • 경로 : order > templates > order > order_create.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
    33
    34
    35
    {% extends 'base.html' %}

    {% block title %}
    Order Checkout
    {% endblock %}

    {% block content %}
    <div class="alert alert-info mt-3">
    Please enter your order information.
    </div>
    <form action="" method="post" class="order-form">
    {% csrf_token %}
    {{form.as_p}}
    <input type="hidden" name="pre_order_id" value="">
    <input type="hidden" name="amount" value="{{cart.get_total_price}}">
    <input type="submit" value="Payment" class="btn btn-lg btn-success">
    </form>
    {% endblock %}

    {% block extra_script %}
    <script type="text/javascript">
    csrf_token = '{{csrf_token}}';
    order_create_url = '{% url 'order_create_ajax' %}';
    order_checkout_url = '{% url 'order_checkout' %}';
    order_validation_url = '{% url 'order_validation' %}';
    order_complete_url = '{% url 'order_complete' %}';
    </script>
    <!-- iamport 사이트에서 해당 스크립트 복사 붙여넣기-->
    <script src="https://cdn.iamport.kr/js/iamport.payment-1.1.5.js" type="text/javascript"></script>

    <!-- 결제창 띄우기 위한 자바스크립트 실행 -->
    {% load staticfiles %}
    <script src="{% static 'js/checkout.js' %}" type="text/javascript"></script>

    {% endblock %}

6. 결제창 실행시키기 위해 필요한 자바스크립트 관련 view의 경로 설정

  • 경로 : order > urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    from django.urls import path

    from .views import *
    urlpatterns = [
    ...
    path('create_ajax/', OrderCreateAjaxView.as_view(), name='order_create_ajax'),
    path('checkout/', OrderCheckoutAjaxView.as_view(), name='order_checkout'),
    path('validation/', OrderImpAjaxView.as_view(), name='order_validation'),
    ]

7. 결제창 실행시키기 위해 필요한 자바스크립트 관련 view 작성

  • 경로 : order > views.py
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    from cart.cart import Cart
    from .models import OrderItem
    from .forms import OrderForm
    from django.views.generic.base import View
    from django.http import JsonResponse

    class OrderCreateAjaxView(View):
    def post(self, request, *args, **kwargs):
    cart = Cart(request)
    form = OrderForm(request.POST)
    if form.is_valid():
    order = form.save()
    for item in cart:
    OrderItem.objects.create(order=order, product=item['product'], price=item['price'], quantity=item['quantity'])
    data = {
    "order_id":order.id
    }
    return JsonResponse(data)
    return JsonResponse({}, status=401)

    from .models import OrderTransaction
    class OrderCheckoutAjaxView(View):
    def post(self, request, *args, **kwargs):
    order_id = request.POST.get('order_id')
    order = Order.objects.get(id=order_id)
    amount = request.POST.get('amount')

    try:
    merchant_order_id = OrderTransaction.objects.create_new(order=order, amount=amount)
    except:
    merchant_order_id = None

    if merchant_order_id is not None:
    data = {
    'works':True,
    'merchant_id':merchant_order_id
    }
    return JsonResponse(data)
    else:
    return JsonResponse({}, status=401)

    class OrderImpAjaxView(View):
    def post(self, request, *args, **kwargs):
    order_id = request.POST.get('order_id')
    merchant_id = request.POST.get('merchant_id')
    imp_id = request.POST.get('imp_id')
    amount = request.POST.get('amount')
    order = Order.objects.filter(pk=order_id)

    if not order.exists():
    return JsonResponse({}, status=401)

    order = order[0]

    transaction = OrderTransaction.objects.filter(order=order, merchant_order_id=merchant_id, amount=amount)

    if not transaction.exists():
    return JsonResponse({}, status=401)

    # 결제 정보 수정
    try:
    exact_transaction = transaction[0]
    exact_transaction.transaction_id = imp_id
    exact_transaction.success = True
    exact_transaction.save()

    # 주문 정보 - 결제 완료로 변경
    order.paid = True
    order.save()
    data = {
    'works':True
    }

    cart = Cart(request)
    cart.clear()

    return JsonResponse(data)
    except Exception as e:
    print("transaction error", e)
    return JsonResponse({"message":str(e)}, status=401)

8. iamport API와 통신하는 함수 작성

  • 경로 : order > iamport.py
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    # iamport 가입 - 설정
    # iamport API와 통신하는 함수 만들기
    # views.py API함수를 이용한 결제 진행
    import requests
    from django.conf import settings

    # pip install requests

    def get_token():
    access_data = {
    'imp_key':settings.IAMPORT_KEY,
    'imp_secret':settings.IAMPORT_SECRET
    }

    url = 'https://api.iamport.kr/users/getToken'

    req = requests.post(url, data=access_data)
    data = req.json()
    if data['code'] is 0:
    return data['response']['access_token']
    else:
    return None

    # iamport에 사전 정보를 보내서 결제 준비
    def payment_prepare(order_id, amount, *args, **kwargs):
    access_token = get_token()
    if access_token:
    access_data = {
    'merchant_uid':order_id,
    'amount':amount
    }

    url = "https://api.iamport.kr/payments/prepare"
    headers = {
    'Authorization':access_token
    }
    req = requests.post(url, data=access_data, headers=headers)
    data = req.json()

    if data['code'] is not 0:
    raise ValueError("API 통신 오류")
    else:
    raise ValueError("토큰 오류")

    # 결제 이후에 해당 하는 주문 번호와 결제 금액으로 진행된 결제가 있는지 찾아주는 함수
    def find_transaction(order_id, *args, **kwargs):
    access_token = get_token()
    if access_token:
    url = "https://api.iamport.kr/payments/find/"+order_id
    headers = {'Authorization':access_token}
    req = requests.post(url, headers=headers)
    data = req.json()
    if data['code'] is 0:
    imp_data = data['response']
    context = {
    'imp_id': imp_data['imp_uid'],
    'merchant_order_id': imp_data['merchant_uid'],
    'amount':imp_data['amount'],
    'status':imp_data['status'],
    'type':imp_data['pay_method'],
    'receipt_url':imp_data['receipt_url']
    }
    return context
    else:
    return None
    else:
    raise ValueError("토큰 오류")

    def cancel_transaction(transaction_id, *args, **kwargs):
    access_token = get_token()
    if access_token:
    url = "https://api.iamport.kr/payments/cancel"
    headers = {'Authorization':access_token}
    access_data = {
    'imp_uid': transaction_id
    }
    print("data", access_data)
    req = requests.post(url, data=access_data, headers=headers)
    data = req.json()
    if data['code'] is 0:
    return data
    else:
    return None
    else:
    raise ValueError("토큰 오류")

9. 결제창 띄우기 위한 자바스크립트 코드 작성

  • 경로 : order > static > js > checkout.js
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    $(function () {
    var IMP = window.IMP;
    IMP.init('iamport 사이트에서 가맹점 식별코드 입력');
    $('.order-form').on('submit', function (e) {
    var amount = parseFloat($('.order-form input[name="amount"]').val().replace(',', ''));
    var type = $('.order-form input[name="type"]:checked').val();
    // 폼 데이터를 기준으로 주문 생성
    var order_id = AjaxCreateOrder(e);
    if (order_id == false) {
    alert('주문 생성 실패\n다시 시도해주세요.');
    return false;
    }

    // 결제 정보 생성
    var merchant_id = AjaxStoreTransaction(e, order_id, amount, type);

    // 결제 정보가 만들어졌으면 iamport로 실제 결제 시도
    if (merchant_id !== '') {
    IMP.request_pay({
    merchant_uid: merchant_id,
    name: 'E-Shop product',
    buyer_name:$('input[name="first_name"]').val()+" "+$('input[name="last_name"]').val(),
    buyer_email:$('input[name="email"]').val(),
    amount: amount
    }, function (rsp) {
    if (rsp.success) {
    var msg = '결제가 완료되었습니다.';
    msg += '고유ID : ' + rsp.imp_uid;
    msg += '상점 거래ID : ' + rsp.merchant_uid;
    msg += '결제 금액 : ' + rsp.paid_amount;
    msg += '카드 승인번호 : ' + rsp.apply_num;
    // 결제가 완료되었으면 비교해서 디비에 반영
    ImpTransaction(e, order_id, rsp.merchant_uid, rsp.imp_uid, rsp.paid_amount);
    } else {
    var msg = '결제에 실패하였습니다.';
    msg += '에러내용 : ' + rsp.error_msg;
    console.log(msg);
    }
    });
    }
    return false;
    });
    });

    // 폼 데이터를 기준으로 주문 생성
    function AjaxCreateOrder(e) {
    e.preventDefault();
    var order_id = '';
    var request = $.ajax({
    method: "POST",
    url: order_create_url,
    async: false,
    data: $('.order-form').serialize()
    });
    request.done(function (data) {
    if (data.order_id) {
    order_id = data.order_id;
    }
    });
    request.fail(function (jqXHR, textStatus) {
    if (jqXHR.status == 404) {
    alert("페이지가 존재하지 않습니다.");
    } else if (jqXHR.status == 403) {
    alert("로그인 해주세요.");
    } else {
    alert("문제가 발생했습니다. 다시 시도해주세요.");
    }
    });
    return order_id;
    }

    // 결제 정보 생성
    function AjaxStoreTransaction(e, order_id, amount, type) {
    e.preventDefault();
    var merchant_id = '';
    var request = $.ajax({
    method: "POST",
    url: order_checkout_url,
    async: false,
    data: {
    order_id : order_id,
    amount: amount,
    type: type,
    csrfmiddlewaretoken: csrf_token,
    }
    });
    request.done(function (data) {
    if (data.works) {
    merchant_id = data.merchant_id;
    }
    });
    request.fail(function (jqXHR, textStatus) {
    if (jqXHR.status == 404) {
    alert("페이지가 존재하지 않습니다.");
    } else if (jqXHR.status == 403) {
    alert("로그인 해주세요.");
    } else {
    alert("문제가 발생했습니다. 다시 시도해주세요.");
    }
    });
    return merchant_id;
    }

    // iamport에 결제 정보가 있는지 확인 후 결제 완료 페이지로 이동
    function ImpTransaction(e, order_id,merchant_id, imp_id, amount) {
    e.preventDefault();
    var request = $.ajax({
    method: "POST",
    url: order_validation_url,
    async: false,
    data: {
    order_id:order_id,
    merchant_id: merchant_id,
    imp_id: imp_id,
    amount: amount,
    csrfmiddlewaretoken: csrf_token
    }
    });
    request.done(function (data) {
    if (data.works) {
    $(location).attr('href', location.origin+order_complete_url+'?order_id='+order_id)
    }
    });
    request.fail(function (jqXHR, textStatus) {
    if (jqXHR.status == 404) {
    alert("페이지가 존재하지 않습니다.");
    } else if (jqXHR.status == 403) {
    alert("로그인 해주세요.");
    } else {
    console.log(jqXHR);
    alert("문제가 발생했습니다. 다시 시도해주세요.");
    }
    });
    }