Final Project 신세계아이앤씨

결제하기 ( 토스페이먼츠 )

pjh8838 2024. 8. 7. 09:20

⚡️ 개발환경

  • Front - React.js
  • Back - SpringBoot, Spring Security, JPA
  • DB - MySql
  • Server - Tomcat 9
  • Tool - sts, VScode
  • Build - maven

⚡️ 시나리오

  1. React에서 API서버로 UI출력 및 결제요청 호출
  2. API 서버로부터 Redirect 받은 정보 Spring 서버로 전달 및 요청
  3. 전달받은 정보이용 Spring에서 Toss API 서버로 결제 승인 요청
  4. 반환받은 승인 결과 DB 저장
  5. 저장된 DB를 React로 응답
  6. 응답된 결과를 React로 UI 출력

 

1. 토스페이먼츠 API 키 발급

토스페이먼츠 개발자센터 - 내 개발정보 - API 키

 

 

2. 리액트 root 폴더 아래에 .env 파일 생성 후 클라이언트 키 삽입

( 나는 .env 파일에 클라이언트 키가 인식이 안돼서 하드코딩해서 넣음 )

 

클라이언트 키 삽입

 

- 인식이 안될때는 하드코딩

 const clientKey = 'test_ck_KNbdOvk5rkOogZa2Qvm4rn07xlzm'; // 하드코딩된 클라이언트 키
 const tossPayments = await loadTossPayments(clientKey);

 

 

3. 스프링부트 application.properties에 시크릿 키 삽입

 

 


결제창 여는 방법

https://www.tosspayments.com/blog/articles/paytech-1

 

결제창을 여는 모든 방법

결제창은 고객이 결제 경험에서 처음 맞이하는 과정이죠. 토스페이먼츠 결제창은 고객에게도, 개발자에게도 최고의 경험을 주고 있어요. 이번 포스트에는 결제 연동을 처음하는 개발자에게 토

www.tosspayments.com

 

1) SDK ( 이걸로 해볼 예정 )

 

2) 카드사, 간편결제사 창 바로 열기


⚡️결제 요청 ( 프론트 )

#설치 커맨드
npm install @tosspayments/payment-sdk --save

 

sdk는 토스페이먼츠 위젯을 불러옴

 

 import { loadTossPayments } from '@tosspayments/payment-sdk';
 
 const token = getAuthToken(); // token (로그인했을때 생성, 아이디별 다른 정보를 보여주기 위함) 값 저장
 // Spring Security antMatchers에 url을 넣으면 누구나 접근가능해서 테스트 용도로는 사용가능하지만 
 // 실제 서비스 이용을 할 때는 antMatchers에 로그인을 제외하고 삭제시키고 로그인시 token값을 받아와 다른   
 // 페이지 접속이 가능하도록 한다.
 const branchId = localStorage.getItem('branchId'); // 저장된 branchId 가져오기

 

발주하기 페이지 리스트 총액

//결제하기
  const handlePayment = async () => {
    const clientKey = 'test_ck_KNbdOvk5rkOogZa2Qvm4rn07xlzm'; // 하드코딩된 클라이언트 키
    const tossPayments = await loadTossPayments(clientKey);

    const paymentData = {
      amount: totalCostPrice, // 결제 금액
      orderId: '1234-4321-0001', // 주문 ID
      orderName: `${branchId} 발주`, // 주문명
      customerName: `${branchId}` // 고객명
    };

    // 오더 카트의 모든 상품 정보를 문자열로 변환
    const itemsString = encodeURIComponent(JSON.stringify(orderCart));

    try {
      // 백엔드에 결제 정보 저장 요청
      await axios.post(`http://localhost:8090/traders/payment/${branchId}`, paymentData,
        // post 방식의 데이터 처리는 headers가 필수!!
        {
          headers: {
            Authorization: `Bearer ${token}`,
            "content-type": 'application/json'
          }
        });

      // URL 쿼리 파라미터에 결제 정보를 추가하여 결제 성공 페이지로 리다이렉트
      const queryString = new URLSearchParams({
        orderId: paymentData.orderId,
        amount: paymentData.amount,
        customerName: paymentData.customerName,
        items: itemsString, // 전체 상품 정보 추가
      }).toString();

      // 결제 요청
      tossPayments.requestPayment('카드', {
        ...paymentData,
        successUrl: `http://localhost:3000/traders/payment/PaymentSuccess?${queryString}`,
        failUrl: 'http://localhost:3000/traders/payment/fail'
      });
    } catch (error) {
      console.error('결제 요청 중 오류 발생:', error);
    }
  };

 

 

⚡️결제 처리 ( 백엔드 )

엔티티

 

DTO

 

Repository ( 기본 기능 )

 

ServiceImpl

 

 

컨트롤러

 

DB에 담기고 결제 처리 완료

 

Toss API 서버로 결제 승인 요청하는 로직 필요


⚡️결제 성공 페이지 ( 프론트 ) ( 결제 완료 버튼 클릭시 데이터가 movement DB에 저장 )

 
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import './PaymentSuccess.css';
import axios from 'axios';
import { getAuthToken } from '../util/auth';


function PaymentSuccess() {
  const location = useLocation();
  const navigate = useNavigate();
  const searchParams = new URLSearchParams(location.search);

  const orderId = searchParams.get('orderId');
  const amount = searchParams.get('amount');
  const customerName = searchParams.get('customerName');
  const items = JSON.parse(decodeURIComponent(searchParams.get('items')) || '[]'); // 모든 상품 정보 가져오기
  const token = getAuthToken(); // token 값 저장

  const handleComplete = async () => {
    //async/await 는 비동기(try, catch처럼 순서대로 실행되지 않는 로직)작업할때 많이 씀
    // 무브먼트에 저장할 데이터 구성
    const movementData = items.map(item => ({
        ordercode: orderId, // 주문 ID
        branchid: customerName, // 지점명 (branchId로 사용)
        gcode: item.goods.gcode, // 상품 코드
        movquantity: item.gcount, // 수량
        movdate: new Date().toISOString().split('T')[0], // 오늘 날짜 (YYYY-MM-DD 형식)
        movstatus: '출고대기' // 고정된 상태
      }));
 
      try {
        // 무브먼트 DB에 저장하기 위한 POST 요청
        const response = await axios.post('http://localhost:8090/traders/movement/ordersave', movementData,
            {
               // post 방식의 데이터 처리는 headers가 필수!!
                headers: {
                    "content-type": 'application/json',
                    Authorization: `Bearer ${token}`,
                }
            });
        console.log('저장 완료:', response.data);
        // 저장 완료 후 대시보드 페이지로 이동
        navigate('/');
      } catch (error) {
        console.error('저장 중 오류 발생:', error);
        alert('저장 중 오류가 발생했습니다. 다시 시도해 주세요.');
      }
  };

  return (
    <div className="payment-success-container">
      <h1 className="success-title">결제 성공</h1>
      <div className="success-details">
        <p className="detail">지점명: <span className="highlight">{customerName}</span></p>
        <p className="detail">주문 ID: <span className="highlight">{orderId}</span></p>
        <p className="detail">결제 금액: <span className="highlight">{amount}</span></p>

        <h2>구매한 상품 목록</h2>
        <ul>
          {items.map((item, index) => (
            <li key={index}>
              상품명: {item.goods?.gname} 상품코드: {item.goods.gcode} 가격: {item.goods?.gcostprice.toLocaleString('ko-KR')}원 수량: {item.gcount}
            </li>
          ))}
        </ul>
      </div>
      <button className="complete-button" onClick={handleComplete}>결제완료</button>
    </div>
  );
}

export default PaymentSuccess;

App.js에 라우팅 설정

{
    path: "/traders/payment/PaymentSuccess",
    element: <PaymentSuccess />
  }

 

 

⚡️결제 성공 후 결제 완료 버튼 클릭시 데이터가 movement DB에 저장 ( 백엔드 )

엔티티, DTO, 레포지토리는 기본

service

 

serviceImpl ( lombok을 써서 엔티티에 게터세터 안적어도 사용가능 )

 

컨트롤러

 


결과

 

 

 

movement DB 저장 완료

 

728x90