iamport 라이브러리 사용하기

Ahyeon, Jung·2023년 12월 2일
0
post-thumbnail
post-custom-banner

결제를 해보자

결제 과정은 프론트에서 결제사에 금액과 정보 등을 가지고 요청을 보내고, 식별가능한 유니크 id를 받은 다음, 해당 id를 백에 전달한다. id를 받은 백은 해당 id를 결제사에 보내 유효한 결제 id인지 확인한 후, 해당 결제 정보를 받아와서 db에 저장한다.

왜 id만 주고 받을까

백은 어떤 경우든 프론트에서 완벽한 정보만 올 것이라고 생각해서는 안됨. 결제 id는 1000원으로 결제 해놓고 100000 만원이라고 서버에 보낼 가능성이 존재하기 때문에 안전하게 결제사에서 정보를 받아와야함.

왜 결제 정보를 결제사와 서버에서 저장할까

결제 내역 history를 제어하기 위함. 특정 기간의 결제 내역을 불러오거나, 특정 결제 대상 품목 필터링 등을 위해서 서버에 저장한다. 결제 내역을 서버에 저장하는게 추후 get할 때 결제사 개입 없어서 더 편함. 데이터 유지가능.

import postPaymentHistory from '@/api/post/postPaymentHistory';
import { useLoginStore } from '@/store';
import { useState } from 'react';
import styles from '../styles/UserId.PaymentSelect.module.scss';

const PaymentSelect = ({
  email,
  username,
  id,
  toggleIsOpenPaymentSelect,
}: {
  email: string;
  username: string;
  id: number;
  toggleIsOpenPaymentSelect: () => void;
}) => {
  const [selectedAmount, setSelectedAmount] = useState<null | number>(null);
  const [customAmount, setCustomAmount] = useState<undefined | number>();

  const handleSelectChange = (amount: number | null) => {
    setSelectedAmount(amount);
    setCustomAmount(0);
  };

  const handleCustomInputChange = (
    event: React.ChangeEvent<HTMLInputElement>,
  ) => {
    const inputValue = Number(event.target.value);
    if (isNaN(inputValue)) {
      alert('숫자를 입력해주세요.');
      setCustomAmount(undefined);
      return;
    } else {
      setSelectedAmount(null);
      setCustomAmount(inputValue);
    }
  };

  return (
    <div className={styles.container}>
      <h2>후원하기</h2>
      <label>
        <input
          type="radio"
          name="paymentAmount"
          value="10000"
          checked={selectedAmount === 10000}
          onChange={() => handleSelectChange(10000)}
        />
        10000원
      </label>

      <label>
        <input
          type="radio"
          name="paymentAmount"
          value="5000"
          checked={selectedAmount === 5000}
          onChange={() => handleSelectChange(5000)}
        />
        5000원
      </label>

      <label>
        <input
          type="radio"
          name="paymentAmount"
          value="3000"
          checked={selectedAmount === 3000}
          onChange={() => handleSelectChange(3000)}
        />
        3000원
      </label>

      <label>
        <input
          type="radio"
          name="paymentAmount"
          value="1000"
          checked={selectedAmount === 1000}
          onChange={() => handleSelectChange(1000)}
        />
        1000원
      </label>

      <label>
        <input
          type="radio"
          name="paymentAmount"
          value="custom"
          checked={selectedAmount === null && customAmount === undefined}
          onChange={() => handleSelectChange(null)}
        />
        직접입력
      </label>

      <input
        type="text"
        placeholder="원하는 금액 입력"
        value={customAmount}
        onChange={handleCustomInputChange}
        disabled={selectedAmount !== null}
      />
      <RequestPay
        payAmount={
          selectedAmount !== null && selectedAmount !== 0
            ? selectedAmount
            : Number(customAmount)
        }
        email={email}
        username={username}
        id={id}
        toggleIsOpenPaymentSelect={toggleIsOpenPaymentSelect}
      />
      <button className={styles.gray} onClick={toggleIsOpenPaymentSelect}>
        후원 취소
      </button>
    </div>
  );
};

export default PaymentSelect;

interface Response {
  apply_num?: number;
  bank_name?: string;
  buyer_addr?: string;
  buyer_email?: string;
  buyer_name?: string;
  buyer_postcode?: string;
  buyer_tel?: string;
  card_name?: string;
  card_quota?: number;
  custom_data?: string;
  imp_uid?: string;
  merchant_uid?: string;
  name?: string;
  paid_amount?: number;
  paid_at?: string;
  pay_method?: string;
  pg_provider?: string;
  pg_tid?: string;
  receipt_url?: string;
  status?: string;
  success: boolean;
}

interface RequestPayParams {
  pg: string;
  pay_method: string;
  merchant_uid: string;
  name: string;
  amount: number;
  buyer_email: string;
  buyer_name: string;
  buyer_tel: string;
  buyer_addr: string;
  buyer_postcode: string;
}

const RequestPay = ({
  email,
  username,
  payAmount,
  id,
  toggleIsOpenPaymentSelect,
}: {
  email: string;
  username: string;
  id: number;
  payAmount: number;
  toggleIsOpenPaymentSelect: () => void;
}) => {
  const code = import.meta.env.VITE_IAMPORT_CODE;

  const { loginUsername, loginEmail } = useLoginStore();

  if (!window.IMP || !code) return;
  const { IMP } = window;
  IMP.init(code);

  const today = new Date();
  const hours = today.getHours(); // 시
  const minutes = today.getMinutes(); // 분
  const seconds = today.getSeconds(); // 초
  const milliseconds = today.getMilliseconds();
  const makeMerchantUid = hours + minutes + seconds + milliseconds;
  const requestPay = () => {
    (
      IMP.request_pay as (
        params: RequestPayParams,
        callback: (rsp: Response) => void,
      ) => void
    )(
      {
        pg: `kakaopay.${import.meta.env.VITE_IAMPORT_KAKAOPAY_ID!}`,
        pay_method: 'card',
        merchant_uid: `IMP_${makeMerchantUid}`,
        name: `${loginUsername}의 ${username} 후원`,
        amount: payAmount,
        buyer_email: loginEmail!,
        buyer_name: loginUsername!,
        buyer_tel: '010-4242-4242',
        buyer_addr: '서울특별시 강남구 신사동',
        buyer_postcode: '01181',
      },
      (rsp: Response) => {
        if (rsp.success) {
          postPaymentHistory({
            sellerName: username,
            sellerEmail: email,
            impUid: rsp.imp_uid,
            sellerId: id,
          });
        } else {
          console.log('결제실패', rsp);
          toggleIsOpenPaymentSelect();
        }
      },
    );
  };

  return (
    <button className={styles.green} onClick={requestPay}>
      후원하기
    </button>
  );
};

결제를 취소해보자

프론트에서는 결제 내역의 유니크값을 가지고 백 서버에 delete 요청을 보냄. 백은 유니크값을 가지고 결제내역 데이터를 찾음. 유니크값 결제 id를 결제사에 보내 결제 취소요청을 보내고, 취소된 정보를 받아서 결제내역 데이터를 변경함.

왜 삭제하지 않고 변경을 하는가

결제 관련 데이터는 중요함. 환불 일시, 환불 금액, 환불 카드사 등 추후 문제가 될 수 있는 부분을 가지고 있어야함

생각하기

전의 모든 trigger는 테스트 버전이다. 사업자등록 필요한데, 법적인 문제가 걸려있어 발들이기 어려움. 유저에게 노출가능한 값은 무엇이 있는가. 특정 정보만으로 결제사로 들어가 접근할 수 없도록 보안이 필요함. 프론트에서 해줄 수 있는 보안 강화는 무엇이 있을까. 수수료는 어디서 얼마나 빠져나가야하는가.

profile
https://a-honey.tistory.com/
post-custom-banner

0개의 댓글