Skip to content

Commit cfe3a1c

Browse files
authored
Sisc1 121 be 베팅시 획득 포인트 베팅 인원수 추가 (#128)
* feat: 베팅시 획득 포인트, 베팅 인원수 추가 * fix: columnDefinition 옵션 제거 * refactor: DB의 UPDATE 쿼리를 직접 날려 값을 증가 Lost Update 문제 해결 * fix: import 수정 * fix: JSON 변환 오류 해결
1 parent c5c0794 commit cfe3a1c

5 files changed

Lines changed: 77 additions & 15 deletions

File tree

backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1919
import org.springframework.validation.annotation.Validated;
2020
import org.springframework.web.bind.annotation.*;
21+
import org.sejongisc.backend.betting.dto.UserBetResponse;
2122

2223
import java.util.List;
2324
import java.util.Optional;
@@ -91,12 +92,12 @@ public ResponseEntity<List<BetRound>> getAllBetRounds() {
9192
}
9293
)
9394
@PostMapping("/user-bets")
94-
public ResponseEntity<UserBet> postUserBet(
95+
public ResponseEntity<UserBetResponse> postUserBet(
9596
@Parameter(hidden = true)
9697
@AuthenticationPrincipal CustomUserDetails principal,
9798
@Valid @RequestBody UserBetRequest userBetRequest) {
9899

99-
UserBet userBet = bettingService.postUserBet(principal.getUserId(), userBetRequest);
100+
UserBetResponse userBet = bettingService.postUserBet(principal.getUserId(), userBetRequest);
100101
return ResponseEntity.ok(userBet);
101102
}
102103

@@ -135,11 +136,11 @@ public ResponseEntity<Void> cancelUserBet(
135136
}
136137
)
137138
@GetMapping("/user-bets/history")
138-
public ResponseEntity<List<UserBet>> getAllUserBets(
139+
public ResponseEntity<List<UserBetResponse>> getAllUserBets(
139140
@Parameter(hidden = true)
140141
@AuthenticationPrincipal CustomUserDetails principal) {
141142

142-
List<UserBet> userBets = bettingService.getAllMyBets(principal.getUserId());
143+
List<UserBetResponse> userBets = bettingService.getAllMyBets(principal.getUserId());
143144
return ResponseEntity.ok(userBets);
144145
}
145146
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.sejongisc.backend.betting.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
import org.sejongisc.backend.betting.entity.BetOption;
6+
import org.sejongisc.backend.betting.entity.BetStatus;
7+
import org.sejongisc.backend.betting.entity.UserBet;
8+
9+
import java.util.UUID;
10+
11+
@Getter
12+
@Builder
13+
public class UserBetResponse {
14+
private UUID userBetId;
15+
private UUID betRoundId;
16+
private String roundTitle; // BetRound의 제목
17+
private String symbol; // BetRound의 종목명
18+
private BetOption option;
19+
private boolean isFree;
20+
private Integer stakePoints;
21+
private BetStatus betStatus;
22+
private Boolean isCorrect; // 결과 (성공 여부)
23+
private Integer earnedPoints;
24+
25+
// Entity -> DTO 변환 메서드
26+
public static UserBetResponse from(UserBet bet) {
27+
return UserBetResponse.builder()
28+
.userBetId(bet.getUserBetId())
29+
// 여기서 bet.getRound()를 호출할 때 영속성 컨텍스트가 살아있어야 함 (Service 내부)
30+
.betRoundId(bet.getRound().getBetRoundID())
31+
.roundTitle(bet.getRound().getTitle())
32+
.symbol(bet.getRound().getSymbol())
33+
.option(bet.getOption())
34+
.isFree(bet.isFree())
35+
.stakePoints(bet.getStakePoints())
36+
.betStatus(bet.getBetStatus())
37+
.isCorrect(bet.isCollect()) // boolean 타입의 Getter는 isCorrect()
38+
.earnedPoints(bet.getPayoutPoints()) // 엔티티 필드명이 payoutPoints임
39+
.build();
40+
}
41+
}

backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class BetRound extends BasePostgresEntity {
4747

4848
@Column(nullable = false)
4949
@Schema(description = "라운드 진행 상태", defaultValue = "false")
50+
@Builder.Default
5051
private boolean status = false; // Todo : Enum 클래스로 변경 고려
5152

5253
@Schema(description = "베팅이 열리는 시각 (유저 참여 시작 시점)")
@@ -78,16 +79,20 @@ public class BetRound extends BasePostgresEntity {
7879

7980
// [추가] 상승(UP) 베팅 통계
8081
@Column(nullable = false, columnDefinition = "integer default 0")
82+
@Builder.Default
8183
private int upBetCount = 0;
8284

8385
@Column(nullable = false)
86+
@Builder.Default
8487
private long upTotalPoints = 0;
8588

8689
// [추가] 하락(DOWN) 베팅 통계
8790
@Column(nullable = false, columnDefinition = "integer default 0")
91+
@Builder.Default
8892
private int downBetCount = 0;
8993

9094
@Column(nullable = false)
95+
@Builder.Default
9196
private long downTotalPoints = 0;
9297

9398
// 라운드가 현재 진행 중인지 여부 반환

backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.springframework.dao.DataIntegrityViolationException;
1818
import org.springframework.stereotype.Service;
1919
import org.springframework.transaction.annotation.Transactional;
20+
import org.sejongisc.backend.betting.dto.UserBetResponse;
2021

2122
import java.math.BigDecimal;
2223
import java.time.LocalDate;
@@ -85,10 +86,16 @@ public boolean setAllowFree() {
8586
}
8687

8788
/**
88-
* 사용자의 전체 베팅 내역 조회
89+
* 사용자의 전체 베팅 내역 조회 (수정됨)
8990
*/
90-
public List<UserBet> getAllMyBets(UUID userId) {
91-
return userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId);
91+
@Transactional(readOnly = true) // 트랜잭션 유지 필수
92+
public List<UserBetResponse> getAllMyBets(UUID userId) {
93+
List<UserBet> userBets = userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId);
94+
95+
// Entity List -> DTO List 변환
96+
return userBets.stream()
97+
.map(UserBetResponse::from)
98+
.toList();
9299
}
93100

94101
/**
@@ -128,45 +135,49 @@ public void closeBetRound() {
128135

129136
/**
130137
* 사용자 베팅 생성
138+
* - 반환 타입을 UserBetResponse(DTO)로 변경하여 LazyInitializationException 방지
139+
* - 통계 업데이트 시 Repository의 @Modifying 쿼리를 사용하여 동시성 문제(Lost Update) 해결
131140
*/
132141
@Transactional
133-
public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) {
142+
public UserBetResponse postUserBet(UUID userId, UserBetRequest userBetRequest) {
143+
// 1. 라운드 조회
134144
BetRound betRound = betRoundRepository.findById(userBetRequest.getRoundId())
135145
.orElseThrow(() -> new CustomException(ErrorCode.BET_ROUND_NOT_FOUND));
136146

147+
// 2. 중복 베팅 검증
137148
if (userBetRepository.existsByRoundAndUserId(betRound, userId)) {
138149
throw new CustomException(ErrorCode.BET_DUPLICATE);
139150
}
140151

152+
// 3. 라운드 상태 검증 (마감 시간 등)
141153
betRound.validate();
142154

143-
// [수정] 유료 베팅인 경우 베팅 포인트 설정
155+
// 4. 베팅 포인트(stake) 결정
144156
int stake = userBetRequest.isFree() ? 0 : userBetRequest.getStakePoints();
145157

146-
// [삭제] 기존 엔티티 메서드 호출 방식 (동시성 문제 발생)
147-
//betRound.addBetStats(userBetRequest.getOption(), stake);
148-
158+
// 5. 라운드 통계 업데이트 (동시성 해결: DB 직접 업데이트)
149159
if (userBetRequest.getOption() == BetOption.RISE) {
150160
betRoundRepository.incrementUpStats(betRound.getBetRoundID(), stake);
151161
} else {
152162
betRoundRepository.incrementDownStats(betRound.getBetRoundID(), stake);
153163
}
154164

165+
// 6. 포인트 차감 및 이력 생성 (유료 베팅인 경우)
155166
if (!userBetRequest.isFree()) {
156167
if (!userBetRequest.isStakePointsValid()) {
157168
throw new CustomException(ErrorCode.BET_POINT_TOO_LOW);
158169
}
159170

160171
pointHistoryService.createPointHistory(
161172
userId,
162-
-userBetRequest.getStakePoints(),
173+
-stake, // 포인트 차감
163174
PointReason.BETTING,
164175
PointOrigin.BETTING,
165176
userBetRequest.getRoundId()
166177
);
167-
stake = userBetRequest.getStakePoints();
168178
}
169179

180+
// 7. UserBet 엔티티 생성
170181
UserBet userBet = UserBet.builder()
171182
.round(betRound)
172183
.userId(userId)
@@ -176,8 +187,11 @@ public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) {
176187
.betStatus(BetStatus.ACTIVE)
177188
.build();
178189

190+
// 8. 저장 및 DTO 변환 반환
179191
try {
180-
return userBetRepository.save(userBet);
192+
UserBet savedBet = userBetRepository.save(userBet);
193+
// 여기서 DTO로 변환해야 트랜잭션 내에서 betRound 정보를 안전하게 가져올 수 있음
194+
return UserBetResponse.from(savedBet);
181195
} catch (DataIntegrityViolationException e) {
182196
throw new CustomException(ErrorCode.BET_DUPLICATE);
183197
}

backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
8787
"org.sejongisc.backend.point.entity",
8888
"org.sejongisc.backend.stock.entity",
8989
"org.sejongisc.backend.template.entity",
90+
"org.sejongisc.backend.betting.entity",
9091
"org.sejongisc.backend.user.entity"
9192
)
9293
.persistenceUnit("primary")

0 commit comments

Comments
 (0)