Skip to content

Commit 5474194

Browse files
authored
Merge pull request #93 from prgrms-web-devcourse-final-project/feat#91
[Feat]: 결제 충전(요청) 기능 구현
2 parents 11f3ef2 + 9525fac commit 5474194

20 files changed

+699
-185
lines changed
Lines changed: 0 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,5 @@
11
package com.backend.domain.cash.controller;
22

3-
import com.backend.domain.cash.dto.CashResponse;
4-
import com.backend.domain.cash.dto.CashTransactionItemResponse;
5-
import com.backend.domain.cash.dto.CashTransactionResponse;
6-
import com.backend.domain.cash.dto.CashTransactionsResponse;
7-
import com.backend.global.rsData.RsData;
8-
import io.swagger.v3.oas.annotations.Operation;
9-
import io.swagger.v3.oas.annotations.tags.Tag;
10-
import org.springframework.http.ResponseEntity;
11-
import org.springframework.web.bind.annotation.*;
12-
13-
import java.util.List;
14-
15-
@RestController
16-
@RequestMapping("api/v1/cashs")
17-
@Tag(name = "Cash", description = "돈 관련 API")
183
public class ApiV1CashController {
194

20-
@GetMapping("/cash")
21-
@Operation(summary = "내 지갑 잔액 조회")
22-
public ResponseEntity<RsData<CashResponse>> getMyCash() {
23-
24-
CashResponse data = CashResponse.builder()
25-
.cashId(77L) // Cash.id..
26-
.memberId(123L)
27-
.balance(155000L) // 현재 잔액(원)..
28-
.createDate("2025-08-01T10:00:00Z") // 생성 시각..
29-
.modifyDate("2025-09-23T12:35:10Z") // 최근 갱신 시각..
30-
.build();
31-
32-
RsData<CashResponse> body =
33-
new RsData<>("200", "지갑 잔액이 조회되었습니다.", data);
34-
35-
return ResponseEntity.ok(body);
36-
}
37-
38-
@GetMapping("/cash/transactions")
39-
@Operation(summary = "내 원장 목록(입금/출금 내역)", description = "입금/출금 내역을 확인합니다.")
40-
public ResponseEntity<RsData<CashTransactionsResponse>> getCashTransactions(
41-
@RequestParam(defaultValue = "1") int page, // 몇 페이지..
42-
@RequestParam(defaultValue = "20") int size // 몇 개씩..
43-
) {
44-
// 충전(입금)..
45-
CashTransactionItemResponse t1 = CashTransactionItemResponse.builder()
46-
.transactionId(98765L)
47-
.cashId(77L)
48-
.type("DEPOSIT")
49-
.amount(50000L) // 입금 = 양수..
50-
.balanceAfter(155000L)
51-
.createdAt("2025-09-23T12:35:10Z")
52-
.related(CashTransactionItemResponse.Related.builder()
53-
.type("PAYMENT")
54-
.id(101L)
55-
.summary("지갑 충전(toss, CARD)")
56-
.build())
57-
.build();
58-
59-
// 낙찰 결제(출금)..
60-
CashTransactionItemResponse t2 = CashTransactionItemResponse.builder()
61-
.transactionId(99001L)
62-
.cashId(77L)
63-
.type("WITHDRAW")
64-
.amount(-32000L) // 출금 = 음수..
65-
.balanceAfter(123000L)
66-
.createdAt("2025-09-30T21:03:00Z")
67-
.related(CashTransactionItemResponse.Related.builder()
68-
.type("BID")
69-
.id(3456L)
70-
.product(CashTransactionItemResponse.Product.builder()
71-
.productId(3L)
72-
.productName("에어팟 프로 2세대")
73-
.thumbnailUrl("https://.../p3.jpg")
74-
.build())
75-
.summary("낙찰 결제 - 에어팟 프로 2세대")
76-
.build())
77-
.build();
78-
79-
// 목록 + 페이징..
80-
List<CashTransactionItemResponse> items = List.of(t1, t2);
81-
CashTransactionsResponse data = CashTransactionsResponse.builder()
82-
.page(page)
83-
.size(size)
84-
.total(2)
85-
.items(items)
86-
.build();
87-
88-
RsData<CashTransactionsResponse> body =
89-
new RsData<>("200", "지갑 원장 목록이 조회되었습니다.", data);
90-
91-
return ResponseEntity.ok(body);
92-
}
93-
94-
@GetMapping("/cash/transactions/{transactionId}")
95-
@Operation(summary = "내 원장 단건 상세 조회", description = "원장 단건 조회")
96-
public ResponseEntity<RsData<CashTransactionResponse>> getCashTransactionDetail(
97-
@PathVariable Long transactionId
98-
) {
99-
CashTransactionResponse data = CashTransactionResponse.builder()
100-
.transactionId(transactionId)
101-
.cashId(77L)
102-
.type("DEPOSIT")
103-
.amount(50000L)
104-
.balanceAfter(155000L)
105-
.createdAt("2025-09-23T12:35:10Z")
106-
.related(CashTransactionResponse.Related.builder()
107-
.type("PAYMENT")
108-
.id(101L)
109-
.links(CashTransactionResponse.Links.builder()
110-
.paymentDetail("/payments/me/101") // 바로가기..
111-
.build())
112-
.build())
113-
.build();
114-
115-
RsData<CashTransactionResponse> body =
116-
new RsData<>("200", "지갑 원장 상세가 조회되었습니다.", data);
117-
118-
return ResponseEntity.ok(body);
119-
}
1205
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.backend.domain.cash.controller;
2+
3+
import com.backend.domain.cash.dto.CashResponse;
4+
import com.backend.domain.cash.dto.CashTransactionItemResponse;
5+
import com.backend.domain.cash.dto.CashTransactionResponse;
6+
import com.backend.domain.cash.dto.CashTransactionsResponse;
7+
import com.backend.global.rsData.RsData;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.*;
12+
13+
import java.util.List;
14+
15+
@RestController
16+
@RequestMapping("api/v1/cashs")
17+
@Tag(name = "Cash", description = "돈 관련 API")
18+
public class ApiV1CashMockController {
19+
20+
@GetMapping("/cash")
21+
@Operation(summary = "내 지갑 잔액 조회")
22+
public ResponseEntity<RsData<CashResponse>> getMyCash() {
23+
24+
CashResponse data = CashResponse.builder()
25+
.cashId(77L) // Cash.id..
26+
.memberId(123L)
27+
.balance(155000L) // 현재 잔액(원)..
28+
.createDate("2025-08-01T10:00:00Z") // 생성 시각..
29+
.modifyDate("2025-09-23T12:35:10Z") // 최근 갱신 시각..
30+
.build();
31+
32+
RsData<CashResponse> body =
33+
new RsData<>("200", "지갑 잔액이 조회되었습니다.", data);
34+
35+
return ResponseEntity.ok(body);
36+
}
37+
38+
@GetMapping("/cash/transactions")
39+
@Operation(summary = "내 원장 목록(입금/출금 내역)", description = "입금/출금 내역을 확인합니다.")
40+
public ResponseEntity<RsData<CashTransactionsResponse>> getCashTransactions(
41+
@RequestParam(defaultValue = "1") int page, // 몇 페이지..
42+
@RequestParam(defaultValue = "20") int size // 몇 개씩..
43+
) {
44+
// 충전(입금)..
45+
CashTransactionItemResponse t1 = CashTransactionItemResponse.builder()
46+
.transactionId(98765L)
47+
.cashId(77L)
48+
.type("DEPOSIT")
49+
.amount(50000L) // 입금 = 양수..
50+
.balanceAfter(155000L)
51+
.createdAt("2025-09-23T12:35:10Z")
52+
.related(CashTransactionItemResponse.Related.builder()
53+
.type("PAYMENT")
54+
.id(101L)
55+
.summary("지갑 충전(toss, CARD)")
56+
.build())
57+
.build();
58+
59+
// 낙찰 결제(출금)..
60+
CashTransactionItemResponse t2 = CashTransactionItemResponse.builder()
61+
.transactionId(99001L)
62+
.cashId(77L)
63+
.type("WITHDRAW")
64+
.amount(-32000L) // 출금 = 음수..
65+
.balanceAfter(123000L)
66+
.createdAt("2025-09-30T21:03:00Z")
67+
.related(CashTransactionItemResponse.Related.builder()
68+
.type("BID")
69+
.id(3456L)
70+
.product(CashTransactionItemResponse.Product.builder()
71+
.productId(3L)
72+
.productName("에어팟 프로 2세대")
73+
.thumbnailUrl("https://.../p3.jpg")
74+
.build())
75+
.summary("낙찰 결제 - 에어팟 프로 2세대")
76+
.build())
77+
.build();
78+
79+
// 목록 + 페이징..
80+
List<CashTransactionItemResponse> items = List.of(t1, t2);
81+
CashTransactionsResponse data = CashTransactionsResponse.builder()
82+
.page(page)
83+
.size(size)
84+
.total(2)
85+
.items(items)
86+
.build();
87+
88+
RsData<CashTransactionsResponse> body =
89+
new RsData<>("200", "지갑 원장 목록이 조회되었습니다.", data);
90+
91+
return ResponseEntity.ok(body);
92+
}
93+
94+
@GetMapping("/cash/transactions/{transactionId}")
95+
@Operation(summary = "내 원장 단건 상세 조회", description = "원장 단건 조회")
96+
public ResponseEntity<RsData<CashTransactionResponse>> getCashTransactionDetail(
97+
@PathVariable Long transactionId
98+
) {
99+
CashTransactionResponse data = CashTransactionResponse.builder()
100+
.transactionId(transactionId)
101+
.cashId(77L)
102+
.type("DEPOSIT")
103+
.amount(50000L)
104+
.balanceAfter(155000L)
105+
.createdAt("2025-09-23T12:35:10Z")
106+
.related(CashTransactionResponse.Related.builder()
107+
.type("PAYMENT")
108+
.id(101L)
109+
.links(CashTransactionResponse.Links.builder()
110+
.paymentDetail("/payments/me/101") // 바로가기..
111+
.build())
112+
.build())
113+
.build();
114+
115+
RsData<CashTransactionResponse> body =
116+
new RsData<>("200", "지갑 원장 상세가 조회되었습니다.", data);
117+
118+
return ResponseEntity.ok(body);
119+
}
120+
}

src/main/java/com/backend/domain/cash/entity/Cash.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import lombok.*;
77

88
@Entity
9-
109
@Getter
1110
@Setter
1211
@NoArgsConstructor

src/main/java/com/backend/domain/cash/entity/CashTransaction.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
public class CashTransaction extends BaseEntity {
1515

1616
// 어떤 지갑의 거래인가요?
17-
@ManyToOne(fetch = FetchType.LAZY) // 한 지갑에 거래가 여러 줄(여러 개)..
17+
@ManyToOne(fetch = FetchType.LAZY) // 한 지갑에 거래가 여러개..
1818
@JoinColumn(name = "cash_id", nullable = false)
1919
private Cash cash;
2020

@@ -28,12 +28,13 @@ public class CashTransaction extends BaseEntity {
2828
private Long amount;
2929

3030
@Column(nullable = false)
31-
private Long balanceAfter; // 이 거래가 끝난 "직후" 잔액..
31+
private Long balanceAfter; // 이 거래가 끝난 직후 잔액..
3232

3333
// 왜 돈이 들어오고/나갔는지? (추적용)..
34-
@Column(length = 30)
35-
private String relatedType; // 예: "PAYMENT", "BID", "REFUND"...
34+
@Column(length = 32, nullable=false)
35+
private String relatedType; // 예: "PAYMENT", "BID"..
3636

37+
@Column(nullable=false)
3738
private Long relatedId; // 예: 결제ID, 입찰ID 등..
3839

3940
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.backend.domain.cash.repository;
2+
3+
import com.backend.domain.cash.entity.Cash;
4+
import com.backend.domain.member.entity.Member;
5+
import jakarta.persistence.LockModeType;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Lock;
8+
9+
import java.util.Optional;
10+
11+
public interface CashRepository extends JpaRepository<Cash, Long> {
12+
13+
Optional<Cash> findByMember(Member m); // 잔액 조회..
14+
15+
@Lock(LockModeType.PESSIMISTIC_WRITE) // 동시성 잠금..
16+
Optional<Cash> findWithLockByMember(Member m);
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.backend.domain.cash.repository;
2+
3+
import com.backend.domain.cash.entity.CashTransaction;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface CashTransactionRepository extends JpaRepository<CashTransaction, Long> {
7+
8+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.backend.domain.payment.controller;
2+
3+
import com.backend.domain.member.entity.Member;
4+
import com.backend.domain.member.service.MemberService;
5+
import com.backend.domain.payment.dto.PaymentRequest;
6+
import com.backend.domain.payment.dto.PaymentResponse;
7+
import com.backend.domain.payment.service.PaymentService;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.http.HttpStatus;
13+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
14+
import org.springframework.security.core.userdetails.User;
15+
import org.springframework.web.bind.annotation.PostMapping;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RestController;
19+
import org.springframework.web.server.ResponseStatusException;
20+
21+
@RestController
22+
@RequiredArgsConstructor
23+
@RequestMapping("/api/v1/payments")
24+
@Tag(name = "Payments", description = "지갑 충전 API")
25+
public class ApiV1PaymentController {
26+
27+
private final MemberService memberService;
28+
private final PaymentService paymentService;
29+
30+
@PostMapping
31+
@Operation(summary="지갑 충전 요청", description="idempotencyKey로 중복 충전 방지")
32+
public PaymentResponse charge(
33+
@AuthenticationPrincipal User user,
34+
@RequestBody @Valid PaymentRequest req
35+
) {
36+
if (user == null) {
37+
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
38+
}
39+
40+
Member actor = memberService.findMemberByEmail(user.getUsername());
41+
42+
return paymentService.charge(actor, req);
43+
}
44+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public ResponseEntity<RsData<PaymentResponse>> charge() {
190190
.methodType("CARD")
191191
.transactionId("pg_tx_abc123")
192192
.createdAt("2025-09-23T12:35:10Z")
193-
.modifyDate("2025-09-23T12:35:10Z")
193+
.paidAt("2025-09-23T12:35:10Z")
194194
.idempotencyKey("topup-20250923-uid123-001")
195195
.cashTransactionId(98765L)
196196
.balanceAfter(155000L)

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.backend.domain.payment.dto;
22

3+
import jakarta.validation.constraints.NotBlank;
34
import lombok.Getter;
45
import lombok.Setter;
56

@@ -8,7 +9,11 @@
89

910
// 결제 수단 등록..
1011
public class PaymentMethodCreateRequest {
12+
13+
@NotBlank
1114
private String type; // "CARD" 또는 "BANK"...
15+
16+
@NotBlank
1217
private String token; // 결제사 토큰..
1318
private String alias; // 별명..
1419
private Boolean isDefault; // 기본 수단 여부..
@@ -23,4 +28,7 @@ public class PaymentMethodCreateRequest {
2328
private String bankCode; // 예: 004
2429
private String bankName; // 예: KB국민은행
2530
private String acctLast4; // "5678"
31+
32+
@NotBlank
33+
private String provider; // "toss", "iamport" 등..
2634
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class PaymentMethodResponse {
1616
private final String type; // "CARD" / "BANK"..
1717
private final String alias; // 별명..
1818
private final Boolean isDefault; // 자주 사용하는 거 등록..
19+
private final String provider; // PG 제공사(토스/아임포트 등)..
1920

2021
// 카드 전용..
2122
private final String brand; // 카드 브랜드(예: SHINHAN)..

0 commit comments

Comments
 (0)