Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.sejongisc.backend.betting.dto.UserBetResponse;

import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -91,12 +92,12 @@ public ResponseEntity<List<BetRound>> getAllBetRounds() {
}
)
@PostMapping("/user-bets")
public ResponseEntity<UserBet> postUserBet(
public ResponseEntity<UserBetResponse> postUserBet(
@Parameter(hidden = true)
@AuthenticationPrincipal CustomUserDetails principal,
@Valid @RequestBody UserBetRequest userBetRequest) {

UserBet userBet = bettingService.postUserBet(principal.getUserId(), userBetRequest);
UserBetResponse userBet = bettingService.postUserBet(principal.getUserId(), userBetRequest);
return ResponseEntity.ok(userBet);
}

Expand Down Expand Up @@ -135,11 +136,11 @@ public ResponseEntity<Void> cancelUserBet(
}
)
@GetMapping("/user-bets/history")
public ResponseEntity<List<UserBet>> getAllUserBets(
public ResponseEntity<List<UserBetResponse>> getAllUserBets(
@Parameter(hidden = true)
@AuthenticationPrincipal CustomUserDetails principal) {

List<UserBet> userBets = bettingService.getAllMyBets(principal.getUserId());
List<UserBetResponse> userBets = bettingService.getAllMyBets(principal.getUserId());
return ResponseEntity.ok(userBets);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.sejongisc.backend.betting.dto;

import lombok.Builder;
import lombok.Getter;
import org.sejongisc.backend.betting.entity.BetOption;
import org.sejongisc.backend.betting.entity.BetStatus;
import org.sejongisc.backend.betting.entity.UserBet;

import java.util.UUID;

@Getter
@Builder
public class UserBetResponse {
private UUID userBetId;
private UUID betRoundId;
private String roundTitle; // BetRound의 제목
private String symbol; // BetRound의 종목명
private BetOption option;
private boolean isFree;
private Integer stakePoints;
private BetStatus betStatus;
private Boolean isCorrect; // 결과 (성공 여부)
private Integer earnedPoints;

// Entity -> DTO 변환 메서드
public static UserBetResponse from(UserBet bet) {
return UserBetResponse.builder()
.userBetId(bet.getUserBetId())
// 여기서 bet.getRound()를 호출할 때 영속성 컨텍스트가 살아있어야 함 (Service 내부)
.betRoundId(bet.getRound().getBetRoundID())
.roundTitle(bet.getRound().getTitle())
.symbol(bet.getRound().getSymbol())
.option(bet.getOption())
.isFree(bet.isFree())
.stakePoints(bet.getStakePoints())
.betStatus(bet.getBetStatus())
.isCorrect(bet.isCollect()) // boolean 타입의 Getter는 isCorrect()
.earnedPoints(bet.getPayoutPoints()) // 엔티티 필드명이 payoutPoints임
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class BetRound extends BasePostgresEntity {

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

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

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

@Column(nullable = false)
@Builder.Default
private long upTotalPoints = 0;

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

@Column(nullable = false)
@Builder.Default
private long downTotalPoints = 0;

// 라운드가 현재 진행 중인지 여부 반환
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.sejongisc.backend.betting.dto.UserBetResponse;

import java.math.BigDecimal;
import java.time.LocalDate;
Expand Down Expand Up @@ -85,10 +86,16 @@ public boolean setAllowFree() {
}

/**
* 사용자의 전체 베팅 내역 조회
* 사용자의 전체 베팅 내역 조회 (수정됨)
*/
public List<UserBet> getAllMyBets(UUID userId) {
return userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId);
@Transactional(readOnly = true) // 트랜잭션 유지 필수
public List<UserBetResponse> getAllMyBets(UUID userId) {
List<UserBet> userBets = userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId);

// Entity List -> DTO List 변환
return userBets.stream()
.map(UserBetResponse::from)
.toList();
}

/**
Expand Down Expand Up @@ -128,45 +135,49 @@ public void closeBetRound() {

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

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

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

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

// [삭제] 기존 엔티티 메서드 호출 방식 (동시성 문제 발생)
//betRound.addBetStats(userBetRequest.getOption(), stake);

// 5. 라운드 통계 업데이트 (동시성 해결: DB 직접 업데이트)
if (userBetRequest.getOption() == BetOption.RISE) {
betRoundRepository.incrementUpStats(betRound.getBetRoundID(), stake);
} else {
betRoundRepository.incrementDownStats(betRound.getBetRoundID(), stake);
}

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

pointHistoryService.createPointHistory(
userId,
-userBetRequest.getStakePoints(),
-stake, // 포인트 차감
PointReason.BETTING,
PointOrigin.BETTING,
userBetRequest.getRoundId()
);
stake = userBetRequest.getStakePoints();
}

// 7. UserBet 엔티티 생성
UserBet userBet = UserBet.builder()
.round(betRound)
.userId(userId)
Expand All @@ -176,8 +187,11 @@ public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) {
.betStatus(BetStatus.ACTIVE)
.build();

// 8. 저장 및 DTO 변환 반환
try {
return userBetRepository.save(userBet);
UserBet savedBet = userBetRepository.save(userBet);
// 여기서 DTO로 변환해야 트랜잭션 내에서 betRound 정보를 안전하게 가져올 수 있음
return UserBetResponse.from(savedBet);
} catch (DataIntegrityViolationException e) {
throw new CustomException(ErrorCode.BET_DUPLICATE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
"org.sejongisc.backend.point.entity",
"org.sejongisc.backend.stock.entity",
"org.sejongisc.backend.template.entity",
"org.sejongisc.backend.betting.entity",
"org.sejongisc.backend.user.entity"
)
.persistenceUnit("primary")
Expand Down