1. 토스페이먼츠 개발자 센터 가입
https://developers.tosspayments.com/
토스페이먼츠 개발자센터
토스페이먼츠 결제 연동 문서, API, 키, 테스트 내역, 웹훅 등록 등 개발에 필요한 정보와 기능을 확인해 보세요. 결제 연동에 필요한 모든 개발자 도구를 제공해 드립니다.
developers.tosspayments.com
https://docs.tosspayments.com/reference/using-api/api-keys
API 키 | 토스페이먼츠 개발자센터
토스페이먼츠 클라이언트 키 및 시크릿 키를 발급받고 사용하는 방법을 알아봅니다. 클라이언트 키는 SDK를 초기화할 때 사용하고 시크릿 키는 API를 호출할 때 사용합니다.
docs.tosspayments.com
2. JSP header에 토스페이먼츠를 호출하는 js 추가
<!-- tosspayments js -->
<script src="https://js.tosspayments.com/v1"></script>
3. 자동 결제창 띄우기
// 베이직 결제창 호출
function basicPay(){
const clientKey = "test_ck_LlDJaYngro1K6KqdMdnG3ezGdRpX"; // 서버에서 전달받은 클라이언트 키
const tossPayments = TossPayments(clientKey);
const customerKey = Math.random().toString(36).substring(2, 12); // 고객 고유키를 서버로부터 받아옵니다.
tossPayments.requestBillingAuth("카드", {
customerKey : customerKey, // 서버에서 전달받은 고객 키
successUrl: "http://localhost:8080/pay/success", // 성공 시 리디렉션 URL
failUrl: "http://localhost:8080/pay/fail" // 실패 시 리디렉션 URL
})
.catch(function (error) {
if (error.code === "USER_CANCEL") {
// 결제 고객이 결제창을 닫았을 때 에러 처리
} else if (error.code === "INVALID_CARD_COMPANY") {
// 유효하지 않은 카드 코드에 대한 에러 처리
}
});
};
4. 카드 정보 전달이 성공했을 시 빌링키 발급 요청과 결제 진행
authKey로 카드 빌링키 발급
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.tosspayments.com/v1/billing/authorizations/issue"))
.header("Authorization", "Basic dGVzdF9za19lcVJHZ1lPMXI1S0FFQjlheUs2MjNRbk4yRXlhOg==")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\"authKey\":\"e_826EDB0730790E96F116FFF3799A65DE\",\"customerKey\":\"aENcQAtPdYbTjGhtQnNVj\"}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
발급 받은 빌링키로 카드 자동결제 승인
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.tosspayments.com/v1/billing/Z_t5vOvQxrj4499PeiJcjen28-V2RyqgYTwN44Rdzk0="))
.header("Authorization", "Basic dGVzdF9za19lcVJHZ1lPMXI1S0FFQjlheUs2MjNRbk4yRXlhOg==")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\"customerKey\":\"aENcQAtPdYbTjGhtQnNVj\",\"amount\":4900,\"orderId\":\"a4CWyWY5m89PNh7xJwhk1\",\"orderName\":\"토스 프라임 구독\",\"customerEmail\":\"customer@email.com\",\"customerName\":\"박토스\",\"taxFreeAmount\":0,\"taxExemptionAmount\":0}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
+) header 부분에 Basic 다음에는 한 칸 띄운 후 발급받은 시크릿 키를 Base64 방식으로 인코딩하여 입력합니다!
https://docs.tosspayments.com/resources/glossary/base64 (<참고)
Controller
@Controller
@RequestMapping("/pay")
public class PaymentController {
/**
* 베이직 결제 로직 처리
*
* @param authKey
* @param customerKey
* @param model
* @return
*/
@GetMapping("/success")
public String success(@RequestParam("authKey") String authKey, @RequestParam("customerKey") String customerKey,
Model model) {
// 권한 확인
if (session.getAttribute("principal") == null) {
throw new DataDeliveryException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.BAD_REQUEST);
}
// 구독 진행중인지 확인
PrincipalDTO principalDTO = (PrincipalDTO) session.getAttribute("principal");
int userPk = principalDTO.getId();
int duplication = paymentService.checkDuplication(userPk);
if (duplication == 0) {
try {
// 주문 ID 생성
String orderId = UUID.randomUUID().toString();
String orderName = "basic";
int amount = 5900;
// 빌링키 발급과 자동 결제 실행
String response = paymentService.authorizeBillingAndAutoPayment(authKey, customerKey, orderId,
orderName, amount, userPk); // 금액은 실제 금액으로 대체
return "payment/success";
} catch (Exception e) {
model.addAttribute("message", e.getMessage());
throw new DataDeliveryException(Define.FAILED_PAYMENT, HttpStatus.BAD_REQUEST);
// return "redirect:/pay/fail";
}
} else {
throw new DataDeliveryException(Define.FAILED_SUBSCRIBE, HttpStatus.BAD_REQUEST);
}
}
}
Service
@Service
@RequiredArgsConstructor
public class PaymentService {
@Value("${payment.toss.test-secret-api-key}")
private String secretKey;
@Autowired
private PaymentRepository paymentRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 결제 요청 로직
* @param authKey
* @param customerKey
* @param orderId
* @param orderName
* @param amount
* @param userPk
* @return
* @throws Exception
*/
@Transactional
public String authorizeBillingAndAutoPayment(String authKey, String customerKey, String orderId, String orderName,
Integer amount, Integer userPk) throws Exception {
String encodedAuthHeader = Base64.getEncoder().encodeToString((secretKey + ":").getBytes());
// 빌링키 발급과 동시에 자동 결제 수행
HttpRequest billingRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.tosspayments.com/v1/billing/authorizations/issue"))
.header("Authorization", "Basic " + encodedAuthHeader).header("Content-Type", "application/json")
.method("POST",
HttpRequest.BodyPublishers
.ofString("{\"authKey\":\"" + authKey + "\",\"customerKey\":\"" + customerKey + "\"}"))
.build();
HttpResponse<String> billingResponse = HttpClient.newHttpClient().send(billingRequest,
HttpResponse.BodyHandlers.ofString());
if (billingResponse.statusCode() == 200) {
JsonNode billingJson = objectMapper.readTree(billingResponse.body());
String billingKey = billingJson.get("billingKey").asText();
// 자동 결제 요청
HttpRequest paymentRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.tosspayments.com/v1/billing/" + billingKey))
.header("Authorization", "Basic " + encodedAuthHeader).header("Content-Type", "application/json")
.method("POST",
HttpRequest.BodyPublishers.ofString(
"{\"customerKey\":\"" + customerKey + "\"," + "\"orderId\":\"" + orderId + "\","
+ "\"orderName\":\"" + orderName + "\"," + "\"amount\":" + amount + "}"))
.build();
HttpResponse<String> paymentResponse = HttpClient.newHttpClient().send(paymentRequest,
HttpResponse.BodyHandlers.ofString());
if (paymentResponse.statusCode() == 200) {
JsonNode paymentJson = objectMapper.readTree(paymentResponse.body());
// 다음 결제일 계산
String dateFormatType = "yyyy-MM-dd";
Date toDay = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormatType);
Calendar cal = Calendar.getInstance();
cal.setTime(toDay);
cal.add(Calendar.MONTH, +1);
String nextDate = simpleDateFormat.format(cal.getTime());
// DTO 변환
PaymentDTO paymentDTO = PaymentDTO.builder()
.userId(userPk)
.lastTransactionKey(paymentJson.get("lastTransactionKey").asText())
.paymentKey(paymentJson.get("paymentKey").asText())
.orderId(paymentJson.get("orderId").asText())
.orderName2(paymentJson.get("orderName").asText())
.billingKey(billingKey)
.customerKey(customerKey)
.amount(amount)
.totalAmount(paymentJson.get("totalAmount").asText())
.requestedAt(paymentJson.get("requestedAt").asText())
.approvedAt(paymentJson.get("approvedAt").asText())
.cancel("N")
.nextPay(nextDate)
.build();
paymentRepository.insert(paymentDTO.toPayment());
paymentRepository.insertSubscribing(paymentDTO.toSubscribing());
return paymentJson.toPrettyString();
} else {
// DTO 변환
PaymentDTO errorPaymentDTO = PaymentDTO.builder()
.userId(userPk).customerKey(customerKey)
.billingKey(billingKey)
.amount(amount)
.orderId(orderId)
.orderName(orderName)
.billingErrorCode(billingResponse.statusCode())
.payErrorCode(paymentResponse.statusCode())
.build();
paymentRepository.insertOrder(errorPaymentDTO.toOrder());
throw new RuntimeException(Define.FAILED_PROCESS_PAYMENT + paymentResponse.body());
}
} else {
throw new RuntimeException(Define.FAILED_ISSUE_BILLINGKEY + billingResponse.body());
}
}
}
환불 구현하기
5. 결제 취소 api
결제 취소는 시크릿키, payment key, cancelReason만 있으면 됩니다.
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel"))
.header("Authorization", "Basic dGVzdF9za19lcVJHZ1lPMXI1S0FFQjlheUs2MjNRbk4yRXlhOg==")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\"cancelReason\":\"고객 변심\"}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
+) header 부분에 Basic 다음에는 한 칸 띄운 후 발급받은 시크릿 키를 Base64 방식으로 인코딩!
기타 코드
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class PaymentDTO{
// 결제 시도시 필요한 값
private Integer userId;
private String customerKey;
private String billingKey;
private Integer amount;
private String orderName;
// 결제 완료 후 받아오는 값
private String lastTransactionKey;
private String paymentKey;
private String orderId;
private String orderName2;
private String requestedAt;
private String approvedAt;
private String totalAmount;
private String cancel;
private Card card;
// 결체 취소시 입력되는 값
private String cancelReason;
private String cancelAmount;
private Integer adminId;
// 다음 정기결제일 계산
private String nextPay;
// 에러 메시지
private Integer billingErrorCode;
private Integer payErrorCode;
private String billingErrorMsg;
private String payErrorMsg;
// 보류
@Data
public class Card {
String number;
String installmentPlanMonths;
String cardType;
String ownerType;
String amount;
}
// Order 객체 반환
public Order toOrder() {
return Order.builder()
.userId(userId)
.customerKey(customerKey)
.billingKey(billingKey)
.amount(amount)
.orderId(orderId)
.orderName(orderName)
.billingErrorCode(billingErrorCode)
.payErrorCode(payErrorCode)
.build();
}
// Payment 객체 반환
public Payment toPayment() {
return Payment.builder()
.userId(userId)
.lastTransactionKey(lastTransactionKey)
.paymentKey(paymentKey)
.orderId(orderId)
.orderName(orderName2)
.billingKey(billingKey)
.customerKey(customerKey)
.amount(amount)
.totalAmount(totalAmount)
.requestedAt(requestedAt)
.approvedAt(approvedAt)
.cancel(cancel)
.build();
}
// Refund 객체 변환
public Refund toRefund() {
return Refund.builder()
.lastTransactionKey(lastTransactionKey)
.paymentKey(paymentKey)
.cancelReason(cancelReason)
.requestedAt(requestedAt)
.approvedAt(approvedAt)
.cancelAmount(cancelAmount)
.adminId(adminId)
.build();
}
// subscribing 객체 반환
public Subscribing toSubscribing() {
return Subscribing.builder()
.subscribing("Y")
.userId(userId)
.orderName(orderName2)
.billingKey(billingKey)
.customerKey(customerKey)
.amount(amount)
.nextPay(nextPay)
.build();
}
}
Order Model (결제 실패시)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Order {
private Integer id;
private Integer userId;
private String customerKey;
private String billingKey;
private Integer amount;
private String orderId;
private String orderName;
private Integer billingErrorCode;
private Integer payErrorCode;
private String created_at;
}
Payment Model (결제 성공시)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Payment{
private Integer id;
private Integer userId;
private String lastTransactionKey;
private String paymentKey;
private String orderId;
private String orderName;
private String billingKey;
private String customerKey;
private Integer amount;
private String totalAmount;
private String requestedAt;
private String approvedAt;
private String cancel;
}
Refund Model (환불시)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Refund{
private Integer id;
private String lastTransactionKey;
private String paymentKey;
private String cancelReason;
private String requestedAt;
private String approvedAt;
private String cancelAmount;
private Integer adminId;
}
'Team project > [파이널] 개발자 매칭 서비스 - Perfectfolio' 카테고리의 다른 글
웹프레임 디자인 시안 (1) | 2024.10.07 |
---|---|
[11~14일차] 문의사항 게시판 CRUD, 페이징, 검색 + JS Fetch (1) | 2024.10.04 |
[4~7일차] 네이버 로그인 연동 구현하기 (0) | 2024.08.23 |
[4~7일차] 카카오 로그인 연동 구현하기 (5) | 2024.08.22 |