Skip to content

Commit e23e54d

Browse files
authored
Merge pull request #111 from prgrms-web-devcourse-final-project/feat#108
[Feat]: 내 지갑 잔액 조회, 내 원장 목록(입금/출금), 내 원장 단건 상세 기능 구현
2 parents 7b0b22e + 21b2ac5 commit e23e54d

File tree

7 files changed

+241
-3
lines changed

7 files changed

+241
-3
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.backend.domain.cash.constant;
2+
3+
public enum RelatedType {
4+
PAYMENT,
5+
BID
6+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,68 @@
11
package com.backend.domain.cash.controller;
22

3+
import com.backend.domain.cash.dto.CashResponse;
4+
import com.backend.domain.cash.dto.CashTransactionResponse;
5+
import com.backend.domain.cash.dto.CashTransactionsResponse;
6+
import com.backend.domain.cash.service.CashService;
7+
import com.backend.domain.member.entity.Member;
8+
import com.backend.domain.member.repository.MemberRepository;
9+
import com.backend.domain.member.service.MemberService;
10+
import io.swagger.v3.oas.annotations.Operation;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.http.HttpStatus;
14+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
15+
import org.springframework.security.core.userdetails.User;
16+
import org.springframework.transaction.annotation.Transactional;
17+
import org.springframework.web.bind.annotation.*;
18+
import org.springframework.web.server.ResponseStatusException;
19+
20+
@RestController
21+
@RequiredArgsConstructor
22+
@RequestMapping("/api/v1")
23+
@Tag(name = "Cash", description = "돈 관련 API")
324
public class ApiV1CashController {
425

26+
private final MemberService memberService;
27+
private final CashService cashService;
28+
29+
@GetMapping("/cash")
30+
@Operation(summary = "내 지갑 잔액 조회", description = "지갑이 없으면 404를 반환합니다.")
31+
@Transactional(readOnly = true)
32+
public CashResponse getMyCash(@AuthenticationPrincipal User user) {
33+
if (user == null) {
34+
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
35+
}
36+
37+
Member actor = memberService.findMemberByEmail(user.getUsername());
38+
return cashService.getMyCashResponse(actor); // 컨트롤러는 위임만
39+
}
40+
41+
@GetMapping("/cash/transactions")
42+
@Operation(summary = "내 원장 목록(입금/출금)", description = "지갑 미생성 시 404 반환, 페이지네이션 추가")
43+
@Transactional(readOnly = true)
44+
public CashTransactionsResponse getMyTransactions(
45+
@AuthenticationPrincipal User user,
46+
@RequestParam(defaultValue = "1") int page,
47+
@RequestParam(defaultValue = "20") int size
48+
) {
49+
if (user == null) {
50+
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
51+
}
52+
Member actor = memberService.findMemberByEmail(user.getUsername());
53+
return cashService.getMyTransactions(actor, page, size);
54+
}
55+
56+
// 단건 상세..
57+
@GetMapping("/cash/transactions/{transactionId}")
58+
@Operation(summary = "내 원장 단건 상세")
59+
@Transactional(readOnly = true)
60+
public CashTransactionResponse getMyTransactionDetail(
61+
@AuthenticationPrincipal User user,
62+
@PathVariable Long transactionId
63+
) {
64+
if (user == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
65+
Member actor = memberService.findMemberByEmail(user.getUsername());
66+
return cashService.getMyTransactionDetail(actor, transactionId);
67+
}
568
}

src/main/java/com/backend/domain/cash/dto/CashTransactionResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ public static class Related {
3131
@Builder
3232
public static class Links {
3333
private final String paymentDetail; // 예: "/payments/me/101"
34+
private final String bidDetail; // 예: /api/v1/bids/products/{productId}
3435
}
3536
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.backend.domain.cash.entity;
22

33
import com.backend.domain.cash.constant.CashTxType;
4+
import com.backend.domain.cash.constant.RelatedType;
45
import com.backend.global.jpa.entity.BaseEntity;
56
import jakarta.persistence.*;
67
import lombok.*;
@@ -31,8 +32,9 @@ public class CashTransaction extends BaseEntity {
3132
private Long balanceAfter; // 이 거래가 끝난 직후 잔액..
3233

3334
// 왜 돈이 들어오고/나갔는지? (추적용)..
34-
@Column(length = 32, nullable=false)
35-
private String relatedType; // 예: "PAYMENT", "BID"..
35+
@Enumerated(EnumType.STRING)
36+
@Column(name = "related_type", length = 32, nullable = false)
37+
private RelatedType relatedType; // 예: "PAYMENT", "BID"..
3638

3739
@Column(nullable=false)
3840
private Long relatedId; // 예: 결제ID, 입찰ID 등..
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
package com.backend.domain.cash.repository;
22

3+
import com.backend.domain.cash.entity.Cash;
34
import com.backend.domain.cash.entity.CashTransaction;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
47
import org.springframework.data.jpa.repository.JpaRepository;
58

9+
import java.util.Optional;
10+
611
public interface CashTransactionRepository extends JpaRepository<CashTransaction, Long> {
712

13+
// 최신순 목록..
14+
Page<CashTransaction> findAllByCashOrderByIdDesc(Cash cash, Pageable pageable);
15+
16+
// 단건 상세..
17+
Optional<CashTransaction> findByIdAndCash(Long id, Cash cash);
818
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.backend.domain.cash.service;
2+
3+
import com.backend.domain.bid.repository.BidRepository;
4+
import com.backend.domain.cash.constant.RelatedType;
5+
import com.backend.domain.cash.dto.CashResponse;
6+
import com.backend.domain.cash.dto.CashTransactionItemResponse;
7+
import com.backend.domain.cash.dto.CashTransactionResponse;
8+
import com.backend.domain.cash.dto.CashTransactionsResponse;
9+
import com.backend.domain.cash.entity.Cash;
10+
import com.backend.domain.cash.entity.CashTransaction;
11+
import com.backend.domain.cash.repository.CashRepository;
12+
import com.backend.domain.cash.repository.CashTransactionRepository;
13+
import com.backend.domain.member.entity.Member;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.data.domain.Page;
16+
import org.springframework.data.domain.PageRequest;
17+
import org.springframework.http.HttpStatus;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
import org.springframework.web.server.ResponseStatusException;
21+
22+
import java.time.Instant;
23+
import java.time.format.DateTimeFormatter;
24+
import java.util.List;
25+
26+
@Service
27+
@RequiredArgsConstructor
28+
public class CashService {
29+
30+
private final CashRepository cashRepository;
31+
private final CashTransactionRepository cashTransactionRepository;
32+
private final BidRepository bidRepository;
33+
34+
@Transactional(readOnly = true)
35+
public CashResponse getMyCashResponse(Member member) {
36+
Cash cash = cashRepository.findByMember(member)
37+
.orElseThrow(() ->
38+
new ResponseStatusException(HttpStatus.NOT_FOUND, "지갑이 아직 생성되지 않았습니다.")
39+
);
40+
41+
return CashResponse.builder()
42+
.cashId(cash.getId())
43+
.memberId(member.getId())
44+
.balance(cash.getBalance())
45+
.createDate(cash.getCreateDate() != null ? cash.getCreateDate().toString() : null)
46+
.modifyDate(cash.getModifyDate() != null ? cash.getModifyDate().toString() : null)
47+
.build();
48+
}
49+
50+
// 원장 목록 조회..
51+
@Transactional(readOnly = true)
52+
public CashTransactionsResponse getMyTransactions(Member member, int page1Base, int size) {
53+
Cash cash = cashRepository.findByMember(member)
54+
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "지갑이 아직 생성되지 않았습니다."));
55+
56+
int page0 = Math.max(0, page1Base - 1);
57+
PageRequest pageable = PageRequest.of(page0, Math.max(1, size));
58+
59+
Page<CashTransaction> page = cashTransactionRepository.findAllByCashOrderByIdDesc(cash, pageable);
60+
61+
List<CashTransactionItemResponse> items = page.getContent().stream()
62+
.map(this::toItem)
63+
.toList();
64+
65+
return CashTransactionsResponse.builder()
66+
.page(page1Base)
67+
.size(size)
68+
.total(page.getTotalElements())
69+
.items(items)
70+
.build();
71+
}
72+
73+
private CashTransactionItemResponse toItem(CashTransaction tx) {
74+
String createdAt = tx.getCreateDate() != null
75+
? tx.getCreateDate().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
76+
: null;
77+
78+
// 관련 정보(ERD의 related_type / related_id 기반)..
79+
CashTransactionItemResponse.Related related = null;
80+
if (tx.getRelatedType() != null && tx.getRelatedId() != null) {
81+
related = CashTransactionItemResponse.Related.builder()
82+
.type(tx.getRelatedType().name())
83+
.id(tx.getRelatedId())
84+
.build();
85+
}
86+
87+
return CashTransactionItemResponse.builder()
88+
.transactionId(tx.getId())
89+
.cashId(tx.getCash().getId())
90+
.type(tx.getType().name())
91+
.amount(tx.getAmount())
92+
.balanceAfter(tx.getBalanceAfter())
93+
.createdAt(createdAt)
94+
.related(related)
95+
.build();
96+
}
97+
98+
// 원장 단건 상세..
99+
@Transactional(readOnly = true)
100+
public CashTransactionResponse getMyTransactionDetail(Member member, Long transactionId) {
101+
102+
// 내 지갑..
103+
Cash cash = cashRepository.findByMember(member)
104+
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "지갑이 아직 생성되지 않았습니다."));
105+
106+
// 내 지갑에 속한 트랜잭션만 조회 (타인의 데이터 차단)..
107+
CashTransaction tx = cashTransactionRepository.findByIdAndCash(transactionId, cash)
108+
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "원장 내역을 찾을 수 없습니다."));
109+
110+
String createdAt = tx.getCreateDate() != null
111+
? tx.getCreateDate().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
112+
: null;
113+
114+
// related + 링크(선택)..
115+
CashTransactionResponse.Related related = null;
116+
if (tx.getRelatedType() != null && tx.getRelatedId() != null) {
117+
CashTransactionResponse.Links links = buildLinks(tx.getRelatedType(), tx.getRelatedId());
118+
119+
related = CashTransactionResponse.Related.builder()
120+
.type(tx.getRelatedType().name())
121+
.id(tx.getRelatedId())
122+
.links(links)
123+
.build();
124+
}
125+
126+
return CashTransactionResponse.builder()
127+
.transactionId(tx.getId())
128+
.cashId(tx.getCash().getId())
129+
.type(tx.getType().name())
130+
.amount(tx.getAmount())
131+
.balanceAfter(tx.getBalanceAfter())
132+
.createdAt(createdAt)
133+
.related(related)
134+
.build();
135+
}
136+
137+
private CashTransactionResponse.Links buildLinks(RelatedType type, Long id) {
138+
if (id == null) return null;
139+
return switch (type) {
140+
case PAYMENT -> CashTransactionResponse.Links.builder()
141+
.paymentDetail("/api/v1/payments/me/" + id)
142+
.build();
143+
case BID -> {
144+
Long productId = bidRepository.findById(id)
145+
.map(b -> b.getProduct().getId())
146+
.orElse(null);
147+
yield productId == null ? null :
148+
CashTransactionResponse.Links.builder()
149+
.bidDetail("/api/v1/bids/products/" + productId)
150+
.build();
151+
}
152+
default -> null; // REFUND/ADJUSTMENT/PROMOTION 등은 링크 없을 수 있음..
153+
};
154+
}
155+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.backend.domain.payment.service;
22

33
import com.backend.domain.cash.constant.CashTxType;
4+
import com.backend.domain.cash.constant.RelatedType;
45
import com.backend.domain.cash.entity.Cash;
56
import com.backend.domain.cash.entity.CashTransaction;
67
import com.backend.domain.cash.repository.CashRepository;
@@ -133,7 +134,7 @@ public PaymentResponse charge(Member actor, PaymentRequest req) {
133134
.type(CashTxType.DEPOSIT) // 입금..
134135
.amount(req.getAmount())
135136
.balanceAfter(newBalance)
136-
.relatedType("PAYMENT") // 근거: PAYMENT..
137+
.relatedType(RelatedType.PAYMENT) // 근거: PAYMENT..
137138
.relatedId(payment.getId())
138139
.build();
139140
cashTransactionRepository.save(tx);

0 commit comments

Comments
 (0)