weekly learning2019. 6. 15. 16:36

온라인 쇼핑에 필요한 결제 모듈에 대해 알아보자.


먼저 결제 api를 제공하는 iamport에 가입한다.

https://www.iamport.kr/


가입하고 로그인하면 관리자 페이지에 접속할 수 있다.


다음으로 시스템설정 메뉴의 하위 메뉴인 PG설정에 들어간다. 그리고 이미지처럼 PG사를 선택하고 테스트모드를 꼭 ON으로 설정하고 스크롤을 내려 전체 저장을 누른다.


PG설정 옆에 내정보 메뉴를 확인하면 코드 내에서 API 관련 설정해줘야 하는 값들을 확인할 수 있다.


아래는 iamport 결제와 관련된 javascript 코드다.

IMP.init 부분을 위 내정보에서 확인할 수 있는 가맹점 식별코드로 바꿔준다.


이 checkout.js 파일은 이번 수업에서는 order app 내에 static/js 디렉토리를 만들어서 그 안에 넣었다.

$(function () {
    var IMP = window.IMP;
    IMP.init('/* 가맹점 식별 코드 */');
    $('.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 {
            alert("문제가 발생했습니다. 다시 시도해주세요.");
        }
    });
}


위 코드에서 아래 부분은 상품명, 구매자 관련 정보를 결제창에서 보여줄 내용들인데 자신의 환경에 맞게 설정해주면 된다.

// 결제 정보가 만들어졌으면 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
            },


그리고 iamport는 jquery를 slim 버전으로 사용하고 있으면 작동하지 않으므로 minified 버전으로 교체한다.

https://code.jquery.com/


order/admin.py

from django.contrib import admin

from .models import Order, OrderItem, OrderTransaction

class TransactionInline(admin.TabularInline):
	model = OrderTransaction

class OrderItemInline(admin.TabularInline):
	model = OrderItem
	raw_id_fields = ['product']

class OrderOption(admin.ModelAdmin):
	list_display = ['id','first_name','last_name','email','paid','created','updated']
	list_editable = ['paid'] # TabularInline : admin에서 테이블 형태로 볼 수 있음
	inlines = [OrderItemInline]

admin.site.register(Order, OrderOption)


iamport 관리자 사이트의 내정보에서 확인할 수 있는 키를 입력해준다.

config/settings.py

IAMPORT_KEY = 'REST API 키'
IAMPORT_SECRET = 'REST API secret'


iamport Document(https://api.iamport.kr/)를 이용해서 만든 결제에 필요한 함수들의 모음 파일이다.

order/iamport.py

# pip install requests

from django.conf import settings

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() # 리퀘스트 자체를 json으로 가져온다
		if data['code'] is 0:
			imp_data = data['response']
			context = {
				'imp_id': imp_data['imp_uid'], # imp_uid == transaction id
				'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("토큰 오류")


장바구니에 담긴 상품의 주문 버튼을 누르면 구매자 정보를 입력하는 페이지를 만든다.

정보 입력 후 Payment 버튼을 누르면 이 페이지에서 결제창이 뜨도록 javascript코드를 사용하는 것을 확인할 수 있다.

order/temapltes/order/order_create.html

{% extends 'base.html' %}

{% block title %}Order Checkout{% endblock %}
{% block extra_style %}{% 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>
<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 %}


다음으로 views.py에 결제 관련 view를 추가해준다.

order/views.py

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):
		return JsonResponse({})

from .models import Order
def order_complete(request):
	# ajax로 주문 완료시, 완료 페이지로 이동하는 경우에 사용
	order_id = request.GET.get('order_id')
	order = Order.objects.filter(pk=order_id)

	if order.exists():
		return render(request, 'order/order_created.html', {'order':order[0]})


다음으로 url을 연결해준다.

order/urls.py

from django.urls import path

from .views import *

urlpatterns = [
	# ...
	path('order_complete/', order_complete, name='order_complete'),
]


order/models.py

class OrderTransactionManager(models.Manager):
	def create_new(self, order, amount, success=None, transaction_status=None):
		if not order:
			raise ValueError("주문이 존재 하지 않습니다")

		temp_uuid = uuid.uuid1()
		temp_order_id = (str(temp_uuid)+str(order.email)).encode('utf-8')
		hashed_order_id = hashlib.sha1(temp_order_id).hexdigest()[:10]
		merchant_order_id = str(hashed_order_id)
		payment_prepare(merchant_ordr_id, amount)

		transaction = self.model(
			order=order,
			merchant_order_id=merchant_order_id,
			amount=amount
		)

		if success is not None:
			transaction.success = success
			transaction.transaction_status = transaction_status

		try:
			transaction.save()
		except Exception as e:
			print("save error", e)

		return transaction.merchant_order_id

	def get_transaction(self, merchant_order_id):
		result = find_transaction(merchant_order_id)
		if result['status'] == 'paid':
			return result
		else:
			return None

class OrderTransaction(models.Model):
	order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='transaction')
	merchant_order_id = models.CharField(max_length=20, blank=True, null=True)
	transaction_id = models.CharField(max_length=120, blank=True, null=True)
	amount = models.IntegerField(default=0)
	transaction_status = models.charField(max_length=20, blank=True, null=True)
	type = models.CharField(max_length=100, blank=True, null=True)
	created = models.DatetimeField(auto_now_add=True)

	objects = OrderTransactionManager()

	def __str__(self):
		return str(self.order.id) + "'s Transaction"

	class Meta:
		ordering = ['-created']


order/urls.py

urlpatterns = [
	# ...
	path('create_ajax/', OrderCreateAajaxView.as_view(), name='order_create_ajax'),
	path('checkout/', OrderCheckoutAajaxView.as_view(), name='order_checkout'),

	path('validation/', OrderImpAjaxView.as_view(), name='order_validation'),
]


마지막으로

$ python manage.py collectstatic


결제 api를 적용 중 payment 버튼을 눌렀는데 결제창이 뜨지 않고 "문제가 발생했습니다. 다시 시도해주세요." 나오면서 401 에러가 뜨는 문제가 발생하였다.


사실 구조가 어려워 문제점을 찾는데 좀 오래 걸렸는데, 처음에는 이번에 추가한 자바스크립트가 적용이 안되고 있나 싶었는데 코드를 조금씩 수정해가면서 확인해보니 이 문제는 아니였다.


자바스크립트를 수정하면서 알게된 게 자바스크립트의 AjaxStoreTransaction 함수에서 request.fail 계속 일어난 거였다.


alert() 을 통해 값이 제대로 들어오고는 있는지 확인해 보았는데, 모든 값이 제대로 들어오고 있었다.


그리고 확인 해본게 url 값을 정하는 order_checkout_url에 문제가 있는듯 싶어 이것과 url로 연결된 view인 OrderCheckoutAjaxView를 확인하기로 했다.


확인해보니 merchant_order_id의 값을 넣는 과정에서 예외가 발생해 계속 값이 None으로 되고 있는 것이였다.


그래서 merchant_order_id = OrderTransaction.objects.create_new(order=order, amount=amount)에서 객체 생성 도중 뭔가 문제가 발생한 것인가 싶어 해당 모델에 가봤더니 OrderTransactionManager()에 들여쓰기가 제대로 되어 있지 않은 것을 확인하고 수정하였더니 결제창이 문제 없이 실행되는 것을 확인할 수 있었다.


다시 한 번 파이썬 문법은 들여쓰기에 정말 민감하다는 것을 알게 되는 시간이였다..


Posted by gawain