온라인 쇼핑에 필요한 결제 모듈에 대해 알아보자.
먼저 결제 api를 제공하는 iamport에 가입한다.
가입하고 로그인하면 관리자 페이지에 접속할 수 있다.
다음으로 시스템설정 메뉴의 하위 메뉴인 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 버전으로 교체한다.
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()에 들여쓰기가 제대로 되어 있지 않은 것을 확인하고 수정하였더니 결제창이 문제 없이 실행되는 것을 확인할 수 있었다.
다시 한 번 파이썬 문법은 들여쓰기에 정말 민감하다는 것을 알게 되는 시간이였다..
'weekly learning' 카테고리의 다른 글
[웹 프로그래밍 스쿨 17주차] 쉬어가는 주 (0) | 2019.06.29 |
---|---|
[웹 프로그래밍 스쿨 16주차] 장고 9주차 (0) | 2019.06.22 |
[웹 프로그래밍 스쿨 14주차] 장고 7주차 (0) | 2019.06.09 |
[웹 프로그래밍 스쿨 13주차] 장고 6주차 (0) | 2019.06.01 |
[웹 프로그래밍 스쿨 12주차] 장고 5주차 (0) | 2019.05.25 |