Spring Boot + React 프로젝트 / 아임포트(iamport) 결제, 취소 기능 구현

fever·2024년 4월 25일
2

💻 구현 기능

결제기능

  1. 리액트에서 IMP 객체로 결제 모듈 띄움
  2. 결제 성공시 백엔드에서 검증 실행 (결제금액 위변조 위험성을 없애기 위함)
  3. 백엔드에서 검증 후 DB에 저장

결제취소 기능

  1. 리액트에서 아임포트로 요청해서 access_token를 받아옴
  2. 해당 토큰으로 결제 취소 실행
  3. 결제 취소 후 기존 결제 DB 삭제

📌 구현 과정

✔️아임포트 설정

https://admin.portone.io/

  • 회원가입 후, 결제 추가
  • 식별코드 받아놓기

✔️결제 기능

1. 아임포트 스크립트 추가

2. 결제모듈 생성

  • 결제버튼을 누르면 온클릭으로 payKg이 실행됨
  • 모듈창이 뜨고, 결제 진행이 완료되면 백엔드로 넘겨서 검증 진행
//결제모듈
  const IMP = window.IMP;
  IMP.init('imp~가맹점식별코드'); /* imp~ : 가맹점 식별코드*/

  //일반결제
  const payKg = () => {
    console.log('일반 결제 시작');

    // IMP 객체를 사용하여 결제를 요청
    IMP.request_pay(
      {
        pg: 'html5_inicis',
        pay_method: 'card',
        merchant_uid: new Date().getTime(), //임의 번호 부여
        name: 'ArtiQ 포인트 충전 / ' + point + 'p',
        amount: 100, //100원으로 고정 진행(선택한 값 받아와도 됨, 테스트라서 100으로 진행)
        buyer_email: userEmail,
        buyer_name: userName,
      },
      function (rsp) {
        // 결제 성공 시
        if (rsp.success) {
          console.log('결제 성공, 백앤드 검증 시작');
          console.log('rsp.imp_uid' + rsp.imp_uid);
          const merchant_uid = rsp.imp_uid; //주문번호
          const imp_uid = rsp.imp_uid; //고유번호

          //백엔드 검증
          pointCheck(imp_uid, merchant_uid);

          //DB 저장
          pointSubmit(rsp.imp_uid);

          alert('포인트 충전 성공');
        } else {
          // 결제 실패 시
         const msg = '결제에 실패하였습니다.';
          msg += '에러내용 : ' + rsp.error_msg;
          alert(msg);
        }
      }
    );
  };

//백엔드 검증 함수
  const pointCheck = async (imp_uid, merchant_uid) => {
    try {
      console.log('백엔드 검증 실행');
      const response = await axios.post('http://localhost:4000/verify/' + imp_uid);

      console.log('결제 검증 완료', response.data);
      //db에 저장
      pointSubmit(merchant_uid);
    } catch (error) {
      console.error('결제 검증 실패', error);
    }
  };

  //결제 정보 전달
  const pointSubmit = async (merchant_uid) => {
    try {
      console.log('넘어가는 결제 번호:' + merchant_uid);
      const response = await axios.post('http://localhost:4000/user/myPage/point/pay', {
        pointCertify: merchant_uid.toString(),
        userEmail: userEmail,
        pointCharge: point,
        pointPay: money,
      });

      // 받은 데이터
      console.log(response.data);
    } catch (error) {
      console.error('결제 테이블 저장 실패', error);
    }
  };

3. 백엔드 결제 검증 서비스

  • 그래들 의존성 추가

  • propertiesd에 iamport.api.keyiamport.api.secret 지정 필요
    (REST API Key = iamport.api.key, REST API Secret = iamport.api.secret)
@Controller
public class PaymentController {

    // 결제 검증 서비스
    private final IamportClient iamportClient;

    public PaymentController(@Value("${iamport.api.key}") String apiKey,
            @Value("${iamport.api.secret}") String apiSecret) {
        this.iamportClient = new IamportClient(apiKey, apiSecret);
    }

    @ResponseBody
    @RequestMapping("/verify/{imp_uid}")
    public IamportResponse<Payment> paymentByImpUid(@PathVariable("imp_uid") String imp_uid)
            throws IamportResponseException, IOException {
        // 데이터와 처음 금액이 일치 확인 이후 결제 성공 실패 여부 리턴
        System.out.println("결제 검증 서비스 실행");
        return iamportClient.paymentByImpUid(imp_uid);
    }

}

✔️결제 취소 기능

  • 취소는 결제와 달리 impKey와 impSecret로 액세스 토큰을 받아서, 해당 토큰과 고유번호(imp_uid) 로 취소 신청을 해야함

0.요청을 위해 프록시 설정(CORS 에러 방지)

1. 아이디, 시크릿 env에 추가

2. 결제 취소 함수

  • 시큐리티 때문에 어드민쪽은 jwt를 같이 보냈음. (시큐리티 없으면 안 보내도 무방)
//결제 취소 함수
//pointCertify = imp_uid
  const onPayCancel = async (pointCertify) => {
    const confirm = window.confirm('결제번호:' + pointCertify + ' / 결제를 취소하시겠습니까?');
    const impKey = process.env.REACT_APP_IMP_KEY;
    const impSecret = process.env.REACT_APP_IMP_SECRET;
    if (confirm) {
      try {
        //access_token 받아옴
        const response = await axios({
          url: '/users/getToken',
          method: 'post',
          headers: { 'Content-Type': 'application/json' },
          data: {
            imp_key: impKey,
            imp_secret: impSecret,
          },
        });
        const { access_token } = response.data.response;
        console.log('받아온 access_token:', access_token);

        //취소요청
        getCancelData(access_token, pointCertify);
      } catch (error) {
        console.error('토큰 추출 에러 발생:', error);
      }
    }
  };

  //취소 요청
  const getCancelData = async (access_token, pointCertify) => {
    try {
      const response = await axios.post(
        '/payments/cancel',
        {
          imp_uid: pointCertify, // 주문번호 (필수)
        },
        {
          headers: {
            'Content-Type': 'application/json',
            Authorization: access_token, // 엑세스 토큰 (필수)
          },
        }
      );
      console.log('결제 취소 완료' + response.data);
      //DB 삭제 요청
      pointTableCancel(pointCertify);
    } catch (error) {
      console.error('결제 취소 에러 발생:', error);
    }
  };

//DB 삭제
  const pointTableCancel = async (pointCertify) => {
    try {
      console.log('넘어가는 결제 번호:' + pointCertify);
      const response = await axios.post(
        'http://localhost:4000/admin/userPointTable/cancel',
        {
          pointCertify: pointCertify,
        },
        { headers: { Authorization: `Bearer ${jwt}` } }
      );

      // 받은 데이터
      console.log(response.data);
      alert('결제 취소 완료');
      fetchUserPoint(); //정보 리로드
    } catch (error) {
      console.error('결제 테이블 삭제 실패', error);
    }
  };

🤔 느낀점과 다음 목표

api 문서를 보면서 하나씩 해보는 재미가 있음 ㅋㅋㅋ
다음엔 결제 취소 부분에도 검증 거쳐서 안전하게 결제 진행 예정!
그리고 리액트 js코드가 넘 더러워서 한 번 정리가 필요할 듯...

참고문서
https://portone.gitbook.io/docs/api/rest-api-access-token
https://portone.gitbook.io/docs/auth/guide-2
https://portone.gitbook.io/docs/api/api-1/api

profile
선명한 삶을 살기 위하여

0개의 댓글