728x90

< 4탄 - 웹 결제 플로우에 맞는 코드리뷰 >

 

이전 글들을 못보신 분들을 위해 앞선 글들의 링크를 제공합니다.

< 1탄 - 프롤로그 >

https://blog.self-made.cloud/240

< 2탄 - Code 먼저 공개합니다. >

https://blog.self-made.cloud/241

< 3탄 - 결제 플로우가 어떻게 되는가 >
https://honeystorage.tistory.com/242 

 

이니시스 연동을 위한 웹 결제 플로우가 아래와 같다고 3탄에서 소개했는데요.

제법 복잡하죠.. (더 단순화 할수 있을것같지만 더 만지기가 싫더라구요 ㅠㅠ)

먼저, 웹 버전의 플로우는 이와같습니다.

1. 클라이언트에서 서버로 상품 및 주문번호 요청
2. 서버에서 주문 상품을 가지고 주문을 생성, 주문 번호를 반환
3. 클라이언트에서 서버로부터 전달받은 주문 번호를 가지고 결제 폼을 서버로 요청
4. 서버에서 결제를 진행하기위한 데이터들을 클라이언트로 반환
5. 클라이언트에서 서버로 이니시스 결제모듈 요청 (팝업형태)
6. 서버에서 이니시스 결제모듈 반환
7. 클라이언트에 팝업 모듈이 열림 + 이때부터 팝업 모듈 상태를 주기적으로 체크
8. 사용자 액션에 따른 처리 (1. 팝업을 닫음, 2. 팝업에서 결제 요청)
  8-1. 닫은경우 팝업 열람전 페이지 유지 혹은 기타 처리
  8-2. 팝업에서 결제요청한 경우 서버로 요청이 전송됨 + 팝업이 닫힘
9. 서버에서 요청 받은 정보들을 통해 결제요청 처리 및 데이터 저장
10. 팝업 모듈이 닫힌게 체크되면 결제요청 완료 페이지로 이동
11. 클라이언트에서 서버로부터 결제 정보를 호출하여 사용자에게 보여줌

 

 

플로우별로 어떤 코드가 어디에 해당하는지 살펴보도록 하겠습니다.

 

1 ~ 4

// client/src/pages/shop

const onOrderRequest = () => {
  if (isMobile()) return mobileOrder();
  else return webOrder();
};

const newOrder = () => {
  return axios.get('/v1/inicis/new/order');
};

const webOrder = async () => {
  const { data: info } = await newOrder();
  const { status, data } = info;

  if (status === 'success') {
    orderNumber = data;

    axios.get('/v1/inicis/request/form', { params: { orderId: data } }).then(resolve => {
      const { data: info } = resolve;
      const { status, data } = info;
      
      if (status === 'success') {
        const form = document.createElement('form');
        form.method = 'post';
        form.acceptCharset = 'UTF-8';
        form.hidden = true;
        form.id = 'pay_form';

        for (let o in data) {
          const input = document.createElement('input');
          input.name = o;
          input.value = data[o];
          input.hidden = true;
          form.appendChild(input);
        }

        document.querySelector('#shop-page').appendChild(form);
        window.INIStdPay.pay('pay_form');
        inicisFormStatus = setInterval(checkInicisFormStatus, 1000);
      } else {
        alert('요청 실패');
      }
    });
  } else {
    alert('요청 실패');
  }
};

 

// server/api/controllers/inicis.controller.js

const getNewOrder = async (req, res) => {
  const { serviceId } = req.query; // 실제로 개발할때는 요청이 들어온 서비스 정보를 주문번호와 함께 저장합니다.

  try {
    const newOrderId = makeOrderId('sample');

    // save order Id
    // ...

    return res.send({ status: 'success', data: newOrderId });
  } catch (error) {
    console.log('create new order: ', error);
    return res.send({ status: 'error', data: 'error' });
  }
};

const getRequestForm = async (req, res) => {
  const { orderId } = req.query;
  if (!orderId) {
    return res.send({ status: 'error', data: 'check parameters' });
  }

  try {
     // 요청된 orderId가 DB에 실제로 존재하는지 체크
  } catch (error) {
    console.log('check exist order: ', error);
    return res.send({ status: 'error', data: 'error' });
  }

  try {
    const price = 100; // 실제로는 요청된 상품 정보를 조회
    const timestamp = dayjs().valueOf();

    const dataset = {
      version: '1.0',
      gopaymethod: 'VBank',
      mid: process.env.MID,
      signature: encryptSha256(`oid=${orderId}&price=${price}&timestamp=${timestamp}`),
      mKey: encryptSha256('SU5JTElURV9UUklQTEVERVNfS0VZU1RS'), // 개발용, 배포용에서는 발급된 key를 사용
      price,
      oid: orderId,
      timestamp,
      currency: 'WON',
      goodname: 'Sample',
      buyername: '홍길동',
      buyertel: '01012341234',
      buyeremail: 'sample@sample.com.kr',
      returnUrl: getServerDomain() + '/v1/inicis/pay/after',
      payViewType: 'popup',
      popupUrl: getServerDomain() + `/v1/inicis/popup/open/${orderId}`,
      closeUrl: '',
    };

    return res.send({ status: 'success', data: dataset });
  } catch (error) {
    console.log('make request form : ', error);
    return res.send({ status: 'error', data: 'error' });
  }
};

이니시스의 웹 결제요청은 폼을 기본으로합니다.

폼을 기본으로 이니시스에서 제공하는 라이브러리를 통해

결제 API요청을 하기 때문에 위와 같은 처리가 필요합니다.

 

popup 형태로 결제 폼을 띄우기 위해서는 위 코드중

payViewType: 'popup',
popupUrl: getServerDomain() + `/v1/inicis/popup/open/${orderId}`

이 부분이 꼭 필요합니다.

팝업이 아니라 페이지 형태의 결제 폼을 제공한다면 위 두줄은 필요없습니다.

 

클라이언트에 띄워주는 팝업에

알맞는 결제모듈을 제공하기 위한 코드들을 살펴볼까요

 

5~7에 해당합니다.

// server/api/controller/inicis.controller.js

const openInicisModule = async (req, res) => {
  const { orderId } = req.params;
  if (!orderId) {
    return res.send({ status: 'error', data: 'check parameters' });
  }

  try {
    // 요청된 orderId가 DB에 실제로 존재하는지 체크
  } catch (error) {
    console.log('check exist order: ', error);
    return res.send({ status: 'error', data: 'error' });
  }

  try {
    const price = 100; // 실제로는 요청된 상품 정보를 조회
    const timestamp = dayjs().valueOf();

    const dataset = {
      version: '1.0',
      gopaymethod: 'VBank',
      mid: process.env.MID,
      signature: encryptSha256(`oid=${orderId}&price=${price}&timestamp=${timestamp}`),
      mKey: encryptSha256('SU5JTElURV9UUklQTEVERVNfS0VZU1RS'), // 개발용, 배포용에서는 발급된 key를 사용
      price,
      oid: orderId,
      timestamp,
      currency: 'WON',
      goodname: 'Sample',
      buyername: '홍길동',
      buyertel: '01012341234',
      buyeremail: 'sample@sample.com.kr',
      returnUrl: getServerDomain() + '/v1/inicis/pay/after',
      payViewType: 'popup',
      popupUrl: getServerDomain() + `/v1/inicis/popup/open/${orderId}`,
      closeUrl: '',
    };

    return res.render('w_inicis', { ...dataset });
  } catch (error) {
    console.log('make request form : ', error);
    return res.send({ status: 'error', data: 'error' });
  }
};
// server/views/w_inicis.pug

doctype html
html(lang="ko")
  head
    title= title
    meta(http-equiv="Content-Type", content="text/html;charset=UTF-8")
    meta(http-equiv="Cache-Control", content="no-cache")

    meta(property="og:title", content= '이니시스 결제')
    meta(property="og:description", content= '이니시스 결제 연동 해보자')
    meta(property="og:type" content= 'website')

    meta(name="description" content= '이니시스 결제 연동 해보자')
    meta(name="type" content= 'website')

    script(src="https://stgstdpay.inicis.com/stdjs/INIStdPay.js", type="text/javascript", charset="UTF-8")
  body
    script. 
      var onPay = function() { INIStdPay.pay("pay_form") }
      onPay()
    form(id="pay_form", method="post", accept-charset="UTF-8", hidden='true') 
      input(type="hidden", name="version", value= version)
      input(type="hidden", name="gopaymethod", value= gopaymethod)
      input(type="hidden", name="mid", value= mid)
      input(type="hidden", name="oid", value= oid)
      input(type="hidden", name="price", value= price)
      input(type="hidden", name="timestamp", value= timestamp)
      input(type="hidden", name="signature", value= signature)
      input(type="hidden", name="mKey", value= mKey)
      input(type="hidden", name="currency", value= currency)
      input(type="hidden", name="goodname", value= goodname)
      input(type="hidden", name="buyername", value= buyername)
      input(type="hidden", name="buyertel", value= buyertel)
      input(type="hidden", name="buyeremail", value= buyeremail)
      input(type="hidden", name="returnUrl", value= returnUrl)
      input(type="hidden", name="closeUrl", value=closeUrl)

 

결제 연동중 제일 중요한게 위 과정이라고 생각하는데요.

바로, 결제 모듈을 뷰 템플릿을 이용해 SSR(server-side rendering) 형태로 제공하는것입니다.

 

여기서 왜 어려움을 겪게되냐면

이니시스 API상에서 보면 "요청과 응답하는 서버의 Domain을 일치시켜라" 라는 부분이 있거든요.

CSR과 RestAPI를 통해 서비스를 제공하는 입장에선는

이 요구사항을 맞추기가 정말 어렵더라고요.

도대체 이걸 어떻게 해야되나... 많은 생각이 들었습니다.

 

그래서 생각해낸게 바로, "결제 모듈 페이지는 서버에서 제공하자!" 입니다.

CSR프로젝트라고해서 페이지를 모두 클라이언트에서

제공하려고 하는것은 나의 딱딱한 생각 때문구나! 싶었습니다.

 

나머지는 이제 원하는대로 요청을 처리하거나

실패/성공 페이지를 제공하는 정도입니다.

 

https://github.com/jaekwangLee/inicis-without-pg 를

참고하여 진행하면 그리 어렵지않아

웹 결제 플로우 코드리뷰는 여기까지로 마치도록 하겠습니다.

 

728x90
반응형