[인프런] 파이썬/장고로 결제 시작하기 (Feat. 아임포트) - 기본편 - 학습 정리 1
* 내가 사용하던 방법에서 더 나은, 효율적인 방법과 스킬에 대한 정보를 얻을 수 있을까 싶어 구매했다.
* 학습 내용을 정리해보고자 한다.
1. 포트원 api를 활용한 결제 프로세스 9단계
1) 유저의 결제 요청 : 웹브라우저 -> 장고서버
2) 결제할 상품 내역을 보여줍니다. (HTML/CSS/JS) : 장고서버 -> 웹브라우저
- 결제할 상품 내역 조회
3) 유저가 "결제하기" 버튼을 클릭하면, 포트원 자바스크립트 API를 통해 결제창을 띄웁니다. : 웹브라우저 -> 포트원 js라이브러리/서버
- 포트원 JS 함 수 호출하여, 결 제창 띄움
4) 결제승인 콜백 (옵션) : 포트원서버 -> 장고서버
- 포트원 파이썬 API 호출 및 결제성공 여부 DB에 기록
5) 결제프로세스 완료 JS 콜백(Callback) 결제는 성공 or 실패 : 포트원 서버 -> 웹브라우저
6) 결제 검증을 요구 (페이지 이동, 혹은 Javascript Ajax 통신) : 웹브라우저 -> 장고서버
- 웹페이지 이 동 JS 호출
7) 유저 결제 건에 대한 결제내역 요청 : 장고서버 -> 포트원 서버
8) 결제내역 API 응답 : 포트원 서버 -> 장고서버
9) 결제 성공/실패 응답 및 페이지 이동 : 장고서버 -> 웹브라우저
- 포트원 파이 썬 API 호출 및 결제성공 여부 DB에 기록
2. 결제요청 JS API, 핵심 파라미터
• IMP.init(가맹점식별코드) : 테스트 가맹점 식별코드 "iamport"
• pg (문자열) : PG사. 미지정 시에 아임포트 관리자에서 지정한 "기본PG"가 호출
• pay_method (문자열) : 결제수단 (디폴트: "card")
• merchant_uid (필수, 문자열) : 가맹점에서 고유 결제 식별자
• name (문자열) : 주문명
• amount (필수, 숫자) : 결제금액
• buyer_name (문자열) : 주문자명
• buyer_email (문자열) : 주문자 이메일
• m_redirect_url (필수, 문자열) : 결제완료 후에 이동할 주소 (hostname을 포함한 절대주소여야합니다.)
- 스마트폰에서의 결제를 지원하기 위해서는 필수 파라미터
3. payment_pay 뷰
• template에서 python 객체를 json 객체로 변환하고 해당 값을 javascript에서 사용하는 방법
- template에서 json으로 변환하기
payment_pay view의 return으로 context에 "payment_props": payment_props 를 전달한다.
=== template 파일에 아래왜 같이 선언한다.
{{payment_props|json_script:"payment-props"}}
=== 위 선언 결과는 아래와 같이 코드가 변환된다.
<script id="payment-props" type="application/json">{"merchant_uid: .......}</script>
- javascript에서 값 가져오기
const json_string = document.querySelector("#payment-props").textContent;
const props = JSON.parse(json_string)
4. 처리 결과 확인하기
- payment check url 정의하기
- javascript에서 정의하기
- window를 사용해 전역함수로 만들어 사용하기
payment_check_url = reverse("payment_check", args=[payment.pk])
...
return render(
request,
"mall_test/payment_pay.html",
{
"payment_check_url": payment_check_url,
"payment_props": payment_props,
},
)
<script>window.PAYMENT_CHECK_URL = "{{payment_check_url}}"</script>
<script>(function(){
...
...
IMP.request_pay(props. function(response) {
location.href = window.PAYMENT_CHECK_URL;
});
...
})();
</script>
5. 현재 버전업 된 포트원을 테스트 하기 위한 추가 작업
- 테스트용 결제대행사 추가
1) 결제연동 -> 연동정보 -> 테스트 탭을 누른다
2) 채널 추가 및 설정 정의
3) 채널 추가
- pg 상점 아이디 필드를 누르면 선택 가능한 mid를 확인할 수 있다. 테스트 결제시 이를 선택하면 나머지는 자동으로 입력 된다.
- pg마다 provider 이름이 다른데, 토스페이먼츠의 경우 uplus이며 아래 이미지에서 확인할 수 있다. 이 값은 "PORTONE_PG_PROVIDER" 환경변수와 settings 설정으로 사용된다.
6. 포트원 결제 에러 메시지 확인 방법
- 포트원 JS 결제 실패에 따른 에러 메시지를 alert 창으로 확인하기 커밋
7. 포트원 정보 설정하기
- .env 파일에 정의하기
PORTONE_PG_PROVIDER=uplus
PORTONE_SHOP_ID=imp56220410
PORTONE_API_KEY=0350482873808750
PORTONE_API_SECRET=YRYHCya7NtoF34tiWLvNa5EKDhgeHt6O3VVnAoihyg055PeXksZCQNFvBZ7WuJqgSphZpJZ7qllmvAQK
- settings.py에 정의하기
# 포트원
PORTONE_PG_PROVIDER = env.str("PORTONE_PG_PROVIDER", default="")
PORTONE_SHOP_ID = env.str("PORTONE_SHOP_ID", default="")
# 포트원 측에서 권장한 포맷이었으나 PG 설정 오류가 발생하여
# PG PROVIDER 값만 활용
# PORTONE_PG = f"{PORTONE_PG_PROVIDER}.{PORTONE_SHOP_ID}"
PORTONE_PG = PORTONE_PG_PROVIDER
PORTONE_API_KEY = env.str("PORTONE_API_KEY", default="")
PORTONE_API_SECRET = env.str("PORTONE_API_SECRET", default="")
8. 포트원 결제내역 검증 및 payment_detail 뷰를 통한 결제내역 조회
- 현 강의는 model에 check 기능을 구현한다.
- 포트원 rest api 호출을 위해 직접 http client 라이브러리 핸들링을 할 수 있지만, iamport-rest-client 라이브러리를 사용하면 더 쉽게 구현할 수 있다.
pip install iamport-rest-client
- .find를 살펴보면 merchant_uid와 imp_uid 를 사용한다.
- imp_uid : 포트원 측에서 할당하는 결제 식별자인데, 결제응답시 얻을 수 있지만, 여기서 따로 저장하고 사용하지 않는다.
- commit 인자를 사용하면, 여러개의 check를 수행한 후, save를 실행하게 만들 수 있다.
- 별도의 통계를 만들고자 한다면, meta 데이터를 jsonfield로 db에 저장하여 사용할 수 있다.
from iamport import Iamport
class Payment(models.Model):
...
def portone_check(self, commit=True):
api = Iamport(
imp_key=settings.PORTONE_API_KEY, imp_secret=settings.PORTONE_API_SECRET
)
try:
meta = api.find(merchant_uid=self.merchant_uid) // 결제 내역을 사전으로 가져오기
except (Iamport.ResponseError, Iamport.HttpError) as e:
logger.error(str(e), exc_info=e)
raise Http404(str(e))
self.status = meta["status"]
self.is_paid_ok = meta["status"] == "paid" and meta["amount"] == self.amount // 결제 검증 부분
if commit:
self.save() //내역 저장
- iamport 라이브러리의 async(비동기) 버전
- 관련 이슈 : https://github.com/iamport/iamport-rest-client-python/issues/56
- 저장소 : https://github.com/rumbarum/iamport-async-rest-client-python
9. 회원가입 App 만들기
- 가입은 CreateView 제너릭 뷰를 사용하고, login과 logout은 django에서 기본으로 제공하는 뷰를 사용한다.
signup = CreateView.as_view(
model=User,
form_class=SignupForm,
template_name="accounts/signup_form.html",
success_url=reverse_lazy("login"),
)
- form 또한 django에서 제공하는 form을 상속받아 사용할 수 있다.
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from accounts.models import User
class SignupForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = User
- LoginView에서 로그인에 성공한 경우
- 로그인 페이지의 url 쿼리스트링에 next인자가 있으면, next인자의 값으로 이동을 한다.
- 만약 next인자값이 없을 경우, settings.LOGIN_REDIRECT_URL에 정의된 값으로 이동을 한다.
- LOGIN_REDIRECT_URL의 기본 설정값 default는 '/accounts/profile/' 이다.
login = LoginView.as_view(
form_class=LoginForm,
template_name="accounts/login_form.html",
)
- form 또한 django에서 제공하는 form을 상속받아 사용할 수 있다.
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from accounts.models import User
class LoginForm(AuthenticationForm):
pass
- LogoutView의 로그아웃에 성공한 경우
- next_page 인자를 사용할 수 있고, 해당 인자는 패턴테임(url reverse)를 사용할 수 있다.
- 따로 설정을 해주지 않으면, next_page = settings.LOGOUT_REDIRECT_VIEW이 된다.
- 그런데 settings.LOGOUT_REDIRECT_VIEW의 default는 None이기 때문에 next_page, LOGOUT_REDIRECT_VIEW 둘 중 하나의 값을 설정해줘야 한다.
#urls.py
#logoutview의 인자로 next_page를 주는 경우
from django.contrib. auth.views import LogoutView
from django.urls import path
urlpatterns =[
path('logout/',LogoutView.as_view(next_page='login'),name='logout'),
]
#template.html
#템플릿에 next인자를 주는 경우
<a href ="{% url 'logout' %}?next={{ request.get_full_path }}">
<h6>로그아웃</h6>
</a>
10. command를 이용해 json으로 만들어진 product dump 데이터 가져오기
- command를 사용하는 기본적인 방법은 동일하기에 중요한 항목만 정리한다.
- dataclass를 사용해 json 데이터를 Item 데이터클래스의 인스턴스들로 구성된 리스트로 생성한다.
class Item:
category_name: str
name: str
price: int
priceUnit: str
desc: str
photo_path: str
class Command(BaseCommand):
help = "Load products from JSON file."
...
item_dict_list = requests.get(json_url).json()
item_list = [Item(**item_dict) for item_dict in item_dict_list]
- get_or_create()에 or을 적용해 default를 정의해 줄 수 있다.
category, __ = Category.objects.get_or_create(name=category_name or "미분류")
- for을 사용할 때 콘솔에서 작업 현황을 퍼센트로 알려주는 함수
for item in tqdm(item_list):
- get_or_create 사용시 default 정의
for item in tqdm(item_list):
category: Category = category_dict[item.category_name or "미분류"]
product, is_created = Product.objects.get_or_create(
category=category,
name=item.name,
defaults={
"description": item.desc,
"price": item.price,
},
)
- requests를 이용해 이미지 데이터 저장하기
photo_url = BASE_URL + item.photo_path
filename = photo_url.rsplit("/", 1)[-1]
photo_data = requests.get(photo_url).content # raw data
product.photo.save(
name=filename,
content=ContentFile(photo_data),
save=True,
)
11. admin 수정하기
- action 항목의 display와 message 항목을 정리한다.
- admin.display 데코레이터를 이용해 custom action을 어떻게 출력할지 정의할 수 있다.
- self.message_user를 이용해 해당 action이 수행된 후, 상단에 처리 결과에 대한 message를 어떻게 출력할지 정의할 수 있다.
@admin.display(description=f"지정 상품을 {Product.Status.ACTIVE.label} 상태로 변경합니다.")
def make_active(self, request, queryset):
count = queryset.update(status=Product.Status.ACTIVE)
self.message_user(
request, f"{count}개의 상품을 {Product.Status.ACTIVE.label} 상태로 변경했습니다."
)
12. template 수정 - sorl-thumbnail 사용하기
- sorl-thumbnail : sorl-thumbnail은 Django에서 이미지 썸네일을 쉽게 생성, 관리할 수 있도록 도와주는 라이브러리이다.
- 주요 기능 :
- 썸네일 생성: 원본 이미지를 다양한 크기로 변환하여 썸네일 이미지를 생성한다.
- 캐싱: 생성된 썸네일 이미지는 파일 시스템에 저장되어, 동일한 요청이 있을 경우 다시 생성하지 않고 저장된 이미지를 사용한다.
- 다양한 옵션: 썸네일 크기, 크롭(crop), 비율 유지 등 다양한 옵션을 설정할 수 있다.
- 간편한 템플릿 태그: HTML 템플릿에서 쉽게 썸네일을 사용할 수 있도록 태그를 제공한다.
- 문서 : https://sorl-thumbnail.readthedocs.io/en/latest/
13. django template의 context_processors 사용하기
- settings.py의 TEMPLATES 항목을 보면, context_prodessors 항목이 있다. 여기에서 반환하는 사전들은 template에서 바로 사용이 가능하다.
- request, auth 등을 사용할 수 있다.
- 예를 들어 {{request.GET.query}}라 선언하면, 현재 url의 query를 가져와 출력한다.
- request.path : query string을 제외한 나머지 주소가 출련된다.
- request.get_full_path : query string을 포함한 전체 주소가 출력된다.
- 페이지 네이션 코드에 인자 값으로 url을 추가하면, 정의한 주소를 기반으로 페이지 값만 변경해 처리해준다.
<div class="mt-3 mb-3">{% bootstrap_pagination page_obj url=request.get_full_path %}</div>