Skip to content

Commit

Permalink
Merge branch 'develop' into feature/JT-28
Browse files Browse the repository at this point in the history
  • Loading branch information
ymkim97 authored Sep 4, 2023
2 parents 7f88c6c + c633683 commit 4e108f5
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ application-*.yml
!application.yml

# End of https://www.toptal.com/developers/gitignore/api/gradle
/module-application/src/main/resources/application-local-payment.yml
7 changes: 7 additions & 0 deletions module-application/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
repositories {
maven { url 'https://jitpack.io' }
}

dependencies {
// Web
implementation 'org.springframework.boot:spring-boot-starter-web'

// I-AM-PORT
implementation 'com.github.iamport:iamport-rest-client-java:0.2.23'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.devtoon.jtoon.payment.presentation;

import java.io.IOException;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.devtoon.jtoon.payment.application.PaymentInfoService;
import com.devtoon.jtoon.payment.request.PaymentInfoDto;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.siot.IamportRestClient.response.IamportResponse;
import com.siot.IamportRestClient.response.Payment;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/payments")
public class PaymentInfoController {
private final PaymentInfoService paymentInfoService;

@PostMapping
public IamportResponse<Payment> verifyIamport(@RequestBody PaymentInfoDto paymentInfoDto)
throws IamportResponseException, IOException {
IamportResponse<Payment> iamportResponse = paymentInfoService.paymentLookup(paymentInfoDto.impUid());
return paymentInfoService.verifyIamport(iamportResponse, paymentInfoDto);
}
}
91 changes: 91 additions & 0 deletions module-application/src/main/resources/templates/payments.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Sample Payment</title>

<!-- jQuery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<!-- iamport.payment.js -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>

<script>
var IMP = window.IMP;
IMP.init("imp27504174");

function requestPay() {
IMP.request_pay({
pg: $("#pg").val(),
pay_method: $("#pay-method").val(),
merchant_uid: "IMP" + new Date().getTime(),
name: '쿠키',
amount: Number($("#amount").val()),
buyer_email: $("#buyer-email").val(),
buyer_name: $("#buyer-name").val(),
buyer_tel: $("#buyer-tel").val()
}, function (rsp) {
// 결제 성공 시
if (rsp.success) {
console.log(rsp);
let data = {
impUid: rsp.imp_uid,
merchantUid: rsp.merchant_uid,
pg: rsp.pg,
payMethod: rsp.pay_method,
productName: rsp.name,
amount: rsp.amount,
buyerEmail: rsp.buyer_email,
buyerName: rsp.buyer_name,
buyerPhone: rsp.buyer_tel
};

// 서버로 해당 정보로 검증 및 주문서 생성 요청
$.ajax({
type: "POST",
url: 'api/v1/payments',
data: JSON.stringify(data),
contentType: "application/json",
dataType: "json",
success: function (result) {
alert("결제 검증 완료");
}, error: function (result) {
console.log(jqXHR.status + ", " + textStatus + ", " + errorThrown);
alert("2. 결제 실패 : " + jqXHR.status + ", " + errorThrown);
}
});
} else {
// 결제 실패 시,
console.log(rsp);
alert('1. 결제 실패 : ' + rsp.error_msg);
}
});
}
</script>
</head>
<body>
이름: <input type="text" id="buyer-name" value="hhj"><br>
이메일: <input type="text" id="buyer-email" value="example@naver.com"><br>
전화번호: <input type="text" id="buyer-tel" value="010-1717-1237"><br>

상품:
<select id='amount'>
<option value='1000'>쿠키 10개 - 1000원</option>
<option value='2000'>쿠키 20개 - 2000원</option>
<option value='3000'>쿠키 30개 - 3000원</option>
<option value='5000'>쿠키 50개 - 5000원</option>
<option value='10000'>쿠키 100개 - 10000원</option>
</select><br>

결제 방법:
<select id='pay-method'>
<option value='card'>카드</option>
</select><br>

결제사:
<select id='pg'>
<option value='kcp'>KG이니시스</option>
</select><br><br>

<button onclick="requestPay()">결제하기</button>
</body>
</html>
10 changes: 10 additions & 0 deletions module-domain/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
repositories {
maven { url 'https://jitpack.io' }
}

dependencies {
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation project(path: ':module-domain-smtp')

// I-AM-PORT
implementation 'com.github.iamport:iamport-rest-client-java:0.2.23'

// Bean Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devtoon.jtoon.member.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.devtoon.jtoon.member.entity.Member;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.devtoon.jtoon.member.entity.LoginType;
import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.entity.Role;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.devtoon.jtoon.payment.application;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.repository.MemberRepository;
import com.devtoon.jtoon.payment.entity.PaymentInfo;
import com.devtoon.jtoon.payment.repository.PaymentInfoRepository;
import com.devtoon.jtoon.payment.request.PaymentInfoDto;
import com.siot.IamportRestClient.IamportClient;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.siot.IamportRestClient.response.IamportResponse;
import com.siot.IamportRestClient.response.Payment;

@Service
@Transactional(readOnly = true)
public class PaymentInfoService {

@Value("${pg.kg-inicis.rest-api-key}")
private String REST_API_KEY;

@Value("${pg.kg-inicis.rest-api-secret}")
private String REST_API_SECRET;

private final IamportClient iamportClient;
private final MemberRepository memberRepository;
private final PaymentInfoRepository paymentInfoRepository;

public PaymentInfoService(MemberRepository memberRepository, PaymentInfoRepository paymentInfoRepository) {
this.iamportClient = new IamportClient(REST_API_KEY, REST_API_SECRET);
this.paymentInfoRepository = paymentInfoRepository;
this.memberRepository = memberRepository;
}

public IamportResponse<Payment> paymentLookup(String impUid) throws IamportResponseException, IOException {
return iamportClient.paymentByImpUid(impUid);
}

@Transactional
public IamportResponse<Payment> verifyIamport(
IamportResponse<Payment> iamportResponse,
PaymentInfoDto paymentInfoDto
) {
validateImpUid(paymentInfoDto);
validateMerchantUid(paymentInfoDto);
validateAmount(iamportResponse, paymentInfoDto.amount());
Member member = memberRepository.findByPhone(paymentInfoDto.buyerPhone())
.orElseThrow(() -> new RuntimeException("member is not found"));
PaymentInfo paymentInfo = paymentInfoDto.toEntity(member);
paymentInfoRepository.save(paymentInfo);

return iamportResponse;
}

private void validateAmount(IamportResponse<Payment> iamportResponse, int amount) {
if (iamportResponse.getResponse().getAmount().intValue() != amount) {
throw new RuntimeException("verify iamport exception");
}
}

private void validateMerchantUid(PaymentInfoDto paymentInfoDto) {
if (paymentInfoRepository.existsByMerchantUid(paymentInfoDto.merchantUid())) {
throw new RuntimeException("merchantUid duplicate");
}
}

private void validateImpUid(PaymentInfoDto paymentInfoDto) {
if (paymentInfoRepository.existsByImpUid(paymentInfoDto.impUid())) {
throw new RuntimeException("impUid duplicate");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.devtoon.jtoon.payment.entity;

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum PG {
KCP("KG_이니시스");

private String name;

private static final Map<String, PG> PG_MAP;

static {
PG_MAP = Collections.unmodifiableMap(Arrays.stream(values())
.collect(Collectors.toMap(PG::getName, Function.identity())));
}

public static PG from(String name) {
return Optional.ofNullable(PG_MAP.get(name))
.orElseThrow(() -> new RuntimeException("현재 서버에 등록되지 않은 Payment Gateway 입니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.devtoon.jtoon.payment.entity;

import java.util.Objects;

import com.devtoon.jtoon.member.entity.Member;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "payments")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PaymentInfo {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "payment_id")
private Long id;

@Column(name = "imp_uid", length = 100, nullable = false, unique = true, updatable = false)
private String impUid; // 포트원 결제 고유번호

@Column(name = "pay_method", length = 100, nullable = false, unique = true, updatable = false)
private String merchantUid; // 가맹점 주문번호

@Column(name = "pg", length = 20, nullable = false)
private PG pg; // 결제사

@Column(name = "pay_method", length = 20, nullable = false)
private String payMethod; // 결제 방법

@Column(name = "product_name", length = 15, nullable = false)
private String productName; // 상품명

@Column(name = "amount", nullable = false)
private int amount; // 결제 금액

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@Builder
private PaymentInfo(
String impUid,
String merchantUid,
PG pg,
String payMethod,
String productName,
int amount,
Member member
) {
validateFieldNotNull(impUid, merchantUid, pg, payMethod, productName, amount, member);
this.impUid = impUid;
this.pg = pg;
this.payMethod = payMethod;
this.merchantUid = merchantUid;
this.productName = productName;
this.amount = amount;
this.member = member;
}

private void validateFieldNotNull(
String impUid,
String merchantUid,
PG pg,
String payMethod,
String productName,
int amount,
Member member
) {
Objects.requireNonNull(impUid, "impUid is null");
Objects.requireNonNull(merchantUid, "merchantUid is null");
Objects.requireNonNull(pg, "pg is null");
Objects.requireNonNull(payMethod, "payMethod is null");
Objects.requireNonNull(productName, "productName is null");
Objects.requireNonNull(member, "member is null");

if (amount <= 0) {
throw new RuntimeException("amount is zero or negative number");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.devtoon.jtoon.payment.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.devtoon.jtoon.payment.entity.PaymentInfo;

@Repository
public interface PaymentInfoRepository extends JpaRepository<PaymentInfo, Long> {

boolean existsByImpUid(String impUid);

boolean existsByMerchantUid(String merchantUid);
}
Loading

0 comments on commit 4e108f5

Please sign in to comment.