Study/django

[인프런] 파이썬/장고로 결제 시작하기 (Feat. 아임포트) - 기본편 - 학습 정리 1

bluebamus 2024. 10. 27.

https://www.inflearn.com/course/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%9E%A5%EA%B3%A0-%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EA%B2%B0%EC%A0%9C?attributionToken=iQHwiAoLCIWb57gGEK-28BQQARokNjcxYWM1ODYtMDAwMC0yNTNlLTljMDItODgzZDI0ZjhkYjY0KgYxOTM3NTgyMLe3jC2o5aoto4CXIpvWty2Ovp0VxcvzF8LwnhXUsp0VkPeyMJruxjCOkckwmNa3LToOZGVmYXVsdF9zZWFyY2hAAUgBaAF6AnNp

 

파이썬/장고로 결제 시작하기 (Feat. 아임포트) - 기본편 강의 | 이진석 - 인프런

이진석 | 웹 서비스 결제 연동에 어려움을 겪고 계신가요? 장고와 아임포트를 통해 여러분의 서비스에 결제를 빠르게 적용해보세요., 인생은 짧습니다. 결제 연동에서도 파이썬/장고를 통해,비

www.inflearn.com

 

* 내가 사용하던 방법에서 더 나은, 효율적인 방법과 스킬에 대한 정보를 얻을 수 있을까 싶어 구매했다.

* 학습 내용을 정리해보고자 한다.

 

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 창으로 확인하기 커밋

      - 링크 : https://github.com/pyhub-kr/course-django-payment-basic/commit/72374acb7ee7a1aee1647de6c009c64f0d4355a4

 

포트원 JS 결제 실패에 따른 에러 메세지를 alert 창으로 보여주기 · pyhub-kr/course-django-payment-basic@72

allieus committed Aug 29, 2024

github.com

 

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>

댓글