Skip to content

Commit 87ec92f

Browse files
authored
Merge pull request #107 from prgrms-web-devcourse-final-project/fix#106
[Fix]: PG 연동, env 제거
2 parents 7a522e1 + 4504ca2 commit 87ec92f

File tree

15 files changed

+468
-22
lines changed

15 files changed

+468
-22
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,8 @@ out/
4040
db_dev.mv.db
4141
db_dev.trace.db
4242
src/main/generated/
43+
44+
### env ###
45+
.env
46+
*.env
47+
.env.*

build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.gradle.kotlin.dsl.implementation
2+
13
plugins {
24
java
35
id("org.springframework.boot") version "3.5.5"
@@ -31,6 +33,8 @@ dependencies {
3133
implementation("org.springframework.boot:spring-boot-starter-web")
3234
implementation("org.springframework.boot:spring-boot-starter-data-redis")
3335
implementation ("org.springframework.boot:spring-boot-starter-websocket")
36+
implementation ("org.springframework.boot:spring-boot-starter-webflux")
37+
3438

3539
// 스프링부트 추가 기능
3640
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")

src/main/java/com/backend/domain/payment/controller/ApiV1PaymentController.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import com.backend.domain.member.service.MemberService;
55
import com.backend.domain.payment.dto.PaymentRequest;
66
import com.backend.domain.payment.dto.PaymentResponse;
7+
import com.backend.domain.payment.dto.TossIssueBillingKeyRequest;
8+
import com.backend.domain.payment.dto.TossIssueBillingKeyResponse;
79
import com.backend.domain.payment.service.PaymentService;
10+
import com.backend.domain.payment.service.TossBillingClient;
811
import io.swagger.v3.oas.annotations.Operation;
912
import io.swagger.v3.oas.annotations.tags.Tag;
1013
import jakarta.validation.Valid;
@@ -26,9 +29,10 @@ public class ApiV1PaymentController {
2629

2730
private final MemberService memberService;
2831
private final PaymentService paymentService;
32+
private final TossBillingClient tossBillingClient;
2933

3034
@PostMapping
31-
@Operation(summary="지갑 충전 요청", description="idempotencyKey로 중복 충전 방지")
35+
@Operation(summary="지갑 충전 요청", description="idempotencyKey로 중복 충전 방지, 일단은 idempotencyKey 아무키로 등록해주세요!.")
3236
public PaymentResponse charge(
3337
@AuthenticationPrincipal User user,
3438
@RequestBody @Valid PaymentRequest req
@@ -41,4 +45,17 @@ public PaymentResponse charge(
4145

4246
return paymentService.charge(actor, req);
4347
}
48+
49+
@PostMapping("/toss/issue-billing-key")
50+
public TossIssueBillingKeyResponse issueBillingKey(
51+
@AuthenticationPrincipal User user,
52+
@RequestBody TossIssueBillingKeyRequest req
53+
) {
54+
if (user == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
55+
56+
Member me = memberService.findMemberByEmail(user.getUsername());
57+
String customerKey = "user-" + me.getId(); // ★ 프런트 값 무시하고 서버에서 생성
58+
59+
return tossBillingClient.issueBillingKey(customerKey, req.getAuthKey());
60+
}
4461
}

src/main/java/com/backend/domain/payment/dto/PaymentRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class PaymentRequest {
1515
private Long paymentMethodId; // 결제수단 ID..
1616

1717
@NotNull
18-
@Min(1)
18+
@Min(100)
1919
private Long amount; // 충전 금액(원)..
2020

2121
@NotBlank
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.backend.domain.payment.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class PgChargeResultResponse {
9+
private final boolean success; // 승인 성공 여부
10+
private final String transactionId; // PG의 paymentKey 등
11+
private final String failureCode; // 실패시 코드(선택)
12+
private final String failureMsg; // 실패시 메시지(선택)
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.backend.domain.payment.dto;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
@Getter
7+
@Setter
8+
public class TossIssueBillingKeyRequest {
9+
private String customerKey;
10+
private String authKey;
11+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.backend.domain.payment.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class TossIssueBillingKeyResponse {
9+
private String billingKey;
10+
private String provider; // "toss"
11+
private String cardBrand; // 선택: 스냅샷에 쓰려면
12+
private String cardNumber; // 선택
13+
private Integer expMonth; // 선택
14+
private Integer expYear; // 선택
15+
}

src/main/java/com/backend/domain/payment/service/PaymentMethodService.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,8 @@ public PaymentMethodResponse create(Long memberId, PaymentMethodCreateRequest re
7474
switch (type) {
7575
case CARD -> {
7676
// 필수값 체크..
77-
if (isBlank(req.getBrand()) || isBlank(req.getLast4()) ||
78-
req.getExpMonth() == null || req.getExpYear() == null) {
79-
throw new IllegalArgumentException("CARD는 brand, last4, expMonth, expYear가 필요합니다.");
77+
if (isBlank(req.getBrand()) || isBlank(req.getLast4())){
78+
throw new IllegalArgumentException("CARD는 brand, last4가 필요합니다. (expMonth/expYear는 선택)");
8079
}
8180
// 카드 필드 채우기..
8281
brand = req.getBrand();

src/main/java/com/backend/domain/payment/service/PaymentService.java

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@
99
import com.backend.domain.payment.constant.PaymentStatus;
1010
import com.backend.domain.payment.dto.PaymentRequest;
1111
import com.backend.domain.payment.dto.PaymentResponse;
12+
import com.backend.domain.payment.dto.PgChargeResultResponse;
1213
import com.backend.domain.payment.entity.Payment;
1314
import com.backend.domain.payment.entity.PaymentMethod;
1415
import com.backend.domain.payment.repository.PaymentMethodRepository;
1516
import com.backend.domain.payment.repository.PaymentRepository;
1617
import lombok.RequiredArgsConstructor;
18+
import lombok.extern.slf4j.Slf4j;
19+
import org.springframework.http.HttpStatus;
1720
import org.springframework.stereotype.Service;
1821
import org.springframework.transaction.annotation.Transactional;
22+
import org.springframework.web.server.ResponseStatusException;
1923

2024
import java.time.LocalDateTime;
21-
import java.time.OffsetDateTime;
2225

26+
@Slf4j
2327
@Service
2428
@RequiredArgsConstructor
2529
public class PaymentService {
@@ -28,19 +32,22 @@ public class PaymentService {
2832
private final PaymentMethodRepository paymentMethodRepository; // 수단..
2933
private final CashRepository cashRepository; // 지갑..
3034
private final CashTransactionRepository cashTransactionRepository; // 원장..
31-
35+
private final TossBillingClient tossBillingClient;
36+
private static final long MIN_AMOUNT = 100L; // 최소 100원
3237
private static final long MAX_AMOUNT_PER_TX = 1_000_000L; // 1회 한도(예시)..
3338

3439
@Transactional // 원자성 보장..
3540
public PaymentResponse charge(Member actor, PaymentRequest req) {
3641

3742
// 입력 검증..
38-
if (req.getAmount() == null || req.getAmount() <= 0) // 금액 필수/양수..
39-
throw new IllegalArgumentException("금액은 1원 이상이어야 합니다.");
40-
if (req.getAmount() > MAX_AMOUNT_PER_TX) // 한도 체크..
41-
throw new IllegalArgumentException("1회 최대 충전 한도를 초과했습니다.");
42-
if (req.getIdempotencyKey() == null || req.getIdempotencyKey().isBlank()) // 멱등키 필수..
43-
throw new IllegalArgumentException("idempotencyKey가 필요합니다.");
43+
if (req.getAmount() == null || req.getAmount() < MIN_AMOUNT)
44+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "최소 결제 금액은 100원입니다.");
45+
46+
if (req.getAmount() > MAX_AMOUNT_PER_TX)
47+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"1회 최대 충전 한도를 초과했습니다.");
48+
49+
if (req.getIdempotencyKey() == null || req.getIdempotencyKey().isBlank())
50+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "idempotencyKey가 필요합니다.");
4451

4552
// 멱등 선조회(같은 회원+키면 기존 결과 그대로 반환)..
4653
var existingOpt = paymentRepository.findByMemberAndIdempotencyKey(actor, req.getIdempotencyKey());
@@ -53,9 +60,13 @@ public PaymentResponse charge(Member actor, PaymentRequest req) {
5360
// 결제수단 검증(본인 소유 + 삭제X + active)..
5461
PaymentMethod pm = paymentMethodRepository
5562
.findByIdAndMemberAndDeletedFalse(req.getPaymentMethodId(), actor) // 삭제 안 된 본인 수단..
56-
.orElseThrow(() -> new IllegalArgumentException("결제수단을 찾을 수 없습니다."));
57-
if (Boolean.FALSE.equals(pm.getActive())) // 비활성 차단..
58-
throw new IllegalArgumentException("비활성화된 결제수단입니다.");
63+
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "결제수단을 찾을 수 없습니다."));
64+
65+
if (Boolean.FALSE.equals(pm.getActive()) || Boolean.TRUE.equals(pm.getDeleted()))
66+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "비활성/삭제된 결제수단입니다.");
67+
68+
if (pm.getToken() == null || pm.getToken().isBlank())
69+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이 결제수단에는 billingKey가 없습니다. 카드 등록(빌링키 발급) 후 사용하세요.");
5970

6071
// payments: PENDING 생성..
6172
Payment payment = Payment.builder()
@@ -77,9 +88,39 @@ public PaymentResponse charge(Member actor, PaymentRequest req) {
7788
.build();
7889
payment = paymentRepository.save(payment);
7990

80-
// (PG 모의) 승인 성공 가정 → 트랜잭션ID 발급..
81-
String txId = "pg_" + payment.getId() + "_" + System.currentTimeMillis(); // 간단 모의..
82-
payment.setTransactionId(txId);
91+
log.info("[PM] id={}, token(billingKey)={}, brand={}, last4={}",
92+
pm.getId(), pm.getToken(), pm.getBrand(), pm.getLast4());
93+
94+
// (PG 모의) → (실제 토스 빌링 결제)로 교체..
95+
String customerKey = "user-" + actor.getId();
96+
PgChargeResultResponse res;
97+
try {
98+
res = tossBillingClient.charge(
99+
pm.getToken(), // billingKey
100+
req.getAmount(), // 금액
101+
req.getIdempotencyKey(), // 멱등키
102+
customerKey
103+
);
104+
} catch (ResponseStatusException ex) {
105+
// 실패 상태로 마킹(선택)
106+
payment.setStatus(PaymentStatus.FAILED);
107+
paymentRepository.save(payment);
108+
109+
// 4xx/5xx 그대로 클라이언트에 전달
110+
throw ex;
111+
}
112+
113+
// (tossBillingClient가 onStatus 에러를 던지도록 바꿨다면 아래는 안전망 정도로 유지)
114+
if (!res.isSuccess()) {
115+
payment.setStatus(PaymentStatus.FAILED);
116+
payment.setTransactionId(res.getTransactionId()); // 있을 수도/없을 수도
117+
paymentRepository.save(payment);
118+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
119+
"PG 승인 실패: " + res.getFailureMsg());
120+
}
121+
122+
// 성공: PG에서 받은 키를 트랜잭션 ID로 저장...
123+
payment.setTransactionId(res.getTransactionId());
83124

84125
// 지갑 잠금 후 잔액 증가 + 원장 DEPOSIT 기록..
85126
Cash cash = cashRepository.findWithLockByMember(actor) // 비관적 잠금..

0 commit comments

Comments
 (0)