From 3ef9beaad00f1f2032e424450b40d120fd66252b Mon Sep 17 00:00:00 2001 From: 20011744 <141496725+cksdid202@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:33:07 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=B2=A0=ED=8C=85=EC=8B=9C=20?= =?UTF-8?q?=ED=9A=8D=EB=93=9D=20=ED=8F=AC=EC=9D=B8=ED=8A=B8,=20=EB=B2=A0?= =?UTF-8?q?=ED=8C=85=20=EC=9D=B8=EC=9B=90=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../betting/controller/BettingController.java | 5 +- .../backend/betting/dto/BetRoundResponse.java | 49 +++++++++++++++++++ .../backend/betting/entity/BetRound.java | 37 ++++++++++++++ .../betting/service/BettingService.java | 15 +++++- 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/org/sejongisc/backend/betting/dto/BetRoundResponse.java diff --git a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java index 36027ea5..4ff2c154 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java @@ -7,6 +7,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; +import org.sejongisc.backend.betting.dto.BetRoundResponse; import org.sejongisc.backend.betting.dto.UserBetRequest; import org.sejongisc.backend.betting.entity.BetRound; import org.sejongisc.backend.betting.entity.Scope; @@ -47,11 +48,11 @@ public class BettingController { } ) @GetMapping("/bet-rounds/{scope}") - public ResponseEntity getTodayBetRound( + public ResponseEntity getTodayBetRound( @Parameter(description = "라운드 범위 (Scope): DAILY 또는 WEEKLY", example = "DAILY") @PathVariable Scope scope ) { - Optional betRound = bettingService.getActiveRound(scope); + Optional betRound = bettingService.getActiveRoundResponse(scope); return betRound .map(ResponseEntity::ok) diff --git a/backend/src/main/java/org/sejongisc/backend/betting/dto/BetRoundResponse.java b/backend/src/main/java/org/sejongisc/backend/betting/dto/BetRoundResponse.java new file mode 100644 index 00000000..c1027337 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/betting/dto/BetRoundResponse.java @@ -0,0 +1,49 @@ +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.BetRound; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Builder +public class BetRoundResponse { + private UUID betRoundId; + private String title; + private String symbol; + private BigDecimal previousClosePrice; + private LocalDateTime openAt; + private LocalDateTime lockAt; + + // 통계 정보 + private int upBetCount; + private int downBetCount; + private long upTotalPoints; + private long downTotalPoints; + + // 예상 획득 포인트 (100포인트 베팅 기준 예시) + private BigDecimal expectedUpReward; + private BigDecimal expectedDownReward; + + public static BetRoundResponse from(BetRound round) { + return BetRoundResponse.builder() + .betRoundId(round.getBetRoundID()) + .title(round.getTitle()) + .symbol(round.getSymbol()) + .previousClosePrice(round.getPreviousClosePrice()) + .openAt(round.getOpenAt()) + .lockAt(round.getLockAt()) + .upBetCount(round.getUpBetCount()) + .downBetCount(round.getDownBetCount()) + .upTotalPoints(round.getUpTotalPoints()) + .downTotalPoints(round.getDownTotalPoints()) + // 예상 배당률 계산 (소수점 등 로직은 기획에 맞춰 조정) + .expectedUpReward(round.getEstimatedRewardMultiplier(BetOption.RISE)) + .expectedDownReward(round.getEstimatedRewardMultiplier(BetOption.FALL)) + .build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java index 7e9aa446..af192d87 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java @@ -76,6 +76,20 @@ public class BetRound extends BasePostgresEntity { @Schema(description = "정산 종가 (결과 비교용)") private BigDecimal settleClosePrice; + // [추가] 상승(UP) 베팅 통계 + @Column(nullable = false, columnDefinition = "integer default 0") + private int upBetCount = 0; + + @Column(nullable = false, columnDefinition = "integer default 0") + private long upTotalPoints = 0; + + // [추가] 하락(DOWN) 베팅 통계 + @Column(nullable = false, columnDefinition = "integer default 0") + private int downBetCount = 0; + + @Column(nullable = false, columnDefinition = "integer default 0") + private long downTotalPoints = 0; + // 라운드가 현재 진행 중인지 여부 반환 public boolean isOpen() { return this.status; @@ -96,6 +110,29 @@ public void close() { this.status = false; } + + // [추가] 통계 업데이트 메서드 (서비스에서 호출) + public void addBetStats(BetOption option, int points) { + if (option == BetOption.RISE) { // UP + this.upBetCount++; + this.upTotalPoints += points; + } else if (option == BetOption.FALL) { // DOWN + this.downBetCount++; + this.downTotalPoints += points; + } + } + + // [추가] 예상 배당률 조회 (예시: 전체 포인트 / 해당 옵션 포인트) + public BigDecimal getEstimatedRewardMultiplier(BetOption option) { + long totalPool = upTotalPoints + downTotalPoints; + long optionPool = (option == BetOption.RISE) ? upTotalPoints : downTotalPoints; + + if (optionPool == 0) return BigDecimal.valueOf(1.0); // 기본 배율 + + // 공식: (전체 베팅 포인트 / 내 옵션 포인트) * 수수료 등 보정 + return BigDecimal.valueOf((double) totalPool / optionPool); + } + // "베팅 가능한 상태인지 검증 public void validate() { if (isClosed() || (lockAt != null && LocalDateTime.now().isAfter(lockAt))) { diff --git a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java index 9f8278c9..ec19ce36 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java @@ -1,6 +1,7 @@ package org.sejongisc.backend.betting.service; import lombok.RequiredArgsConstructor; +import org.sejongisc.backend.betting.dto.BetRoundResponse; import org.sejongisc.backend.betting.dto.PriceResponse; import org.sejongisc.backend.betting.dto.UserBetRequest; import org.sejongisc.backend.betting.entity.*; @@ -139,7 +140,13 @@ public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) { betRound.validate(); - int stake = 0; + // [수정] 유료 베팅인 경우 베팅 포인트 설정 + int stake = userBetRequest.isFree() ? 0 : userBetRequest.getStakePoints(); + + // [추가] 라운드 통계 업데이트 (인원수, 포인트 증가) + betRound.addBetStats(userBetRequest.getOption(), stake); + // 변경감지(Dirty Checking)에 의해 betRoundRepository.save(betRound) 없이도 저장되지만, + // 명시적으로 저장해도 무방. if (!userBetRequest.isFree()) { if (!userBetRequest.isStakePointsValid()) { @@ -172,6 +179,12 @@ public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) { } } + // [추가] getActiveRound 반환 타입 변경 대응 메서드 (Controller에서 사용) + public Optional getActiveRoundResponse(Scope type) { + return betRoundRepository.findTopByStatusTrueAndScopeOrderByOpenAtDesc(type) + .map(BetRoundResponse::from); + } + /** * 사용자 베팅 취소 */ From 898bdd3134203ee144cb4f81bbb9c81d15c50e4c Mon Sep 17 00:00:00 2001 From: 20011744 <141496725+cksdid202@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:21:11 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20columnDefinition=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/betting/entity/BetRound.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java index af192d87..bae72b2e 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java @@ -80,14 +80,14 @@ public class BetRound extends BasePostgresEntity { @Column(nullable = false, columnDefinition = "integer default 0") private int upBetCount = 0; - @Column(nullable = false, columnDefinition = "integer default 0") + @Column(nullable = false) private long upTotalPoints = 0; // [추가] 하락(DOWN) 베팅 통계 @Column(nullable = false, columnDefinition = "integer default 0") private int downBetCount = 0; - @Column(nullable = false, columnDefinition = "integer default 0") + @Column(nullable = false) private long downTotalPoints = 0; // 라운드가 현재 진행 중인지 여부 반환 @@ -110,18 +110,6 @@ public void close() { this.status = false; } - - // [추가] 통계 업데이트 메서드 (서비스에서 호출) - public void addBetStats(BetOption option, int points) { - if (option == BetOption.RISE) { // UP - this.upBetCount++; - this.upTotalPoints += points; - } else if (option == BetOption.FALL) { // DOWN - this.downBetCount++; - this.downTotalPoints += points; - } - } - // [추가] 예상 배당률 조회 (예시: 전체 포인트 / 해당 옵션 포인트) public BigDecimal getEstimatedRewardMultiplier(BetOption option) { long totalPool = upTotalPoints + downTotalPoints; From 47dd612004fa7e6d234bd02e5a368319de6bb676 Mon Sep 17 00:00:00 2001 From: 20011744 <141496725+cksdid202@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:22:35 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20DB=EC=9D=98=20UPDATE=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EB=A5=BC=20=EC=A7=81=EC=A0=91=20=EB=82=A0?= =?UTF-8?q?=EB=A0=A4=20=EA=B0=92=EC=9D=84=20=EC=A6=9D=EA=B0=80=20Lost=20Up?= =?UTF-8?q?date=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../betting/repository/BetRoundRepository.java | 13 +++++++++++++ .../backend/betting/service/BettingService.java | 12 ++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java b/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java index aedc4bfc..b9064fdd 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java @@ -1,8 +1,11 @@ package org.sejongisc.backend.betting.repository; +import io.lettuce.core.dynamic.annotation.Param; import org.sejongisc.backend.betting.entity.BetRound; import org.sejongisc.backend.betting.entity.Scope; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import java.time.LocalDateTime; import java.util.List; @@ -17,4 +20,14 @@ public interface BetRoundRepository extends JpaRepository { List findByStatusTrueAndLockAtLessThanEqual(LocalDateTime now); List findByStatusFalseAndSettleAtIsNullAndLockAtLessThanEqual(LocalDateTime now); + + // [추가] 상승(UP) 통계 원자적 업데이트 + @Modifying(clearAutomatically = true) // 쿼리 실행 후 영속성 컨텍스트 초기화 (데이터 동기화) + @Query("UPDATE BetRound b SET b.upBetCount = b.upBetCount + 1, b.upTotalPoints = b.upTotalPoints + :points WHERE b.betRoundID = :id") + void incrementUpStats(@Param("id") UUID id, @Param("points") long points); + + // [추가] 하락(DOWN) 통계 원자적 업데이트 + @Modifying(clearAutomatically = true) + @Query("UPDATE BetRound b SET b.downBetCount = b.downBetCount + 1, b.downTotalPoints = b.downTotalPoints + :points WHERE b.betRoundID = :id") + void incrementDownStats(@Param("id") UUID id, @Param("points") long points); } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java index ec19ce36..cea5bf70 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java @@ -143,10 +143,14 @@ public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) { // [수정] 유료 베팅인 경우 베팅 포인트 설정 int stake = userBetRequest.isFree() ? 0 : userBetRequest.getStakePoints(); - // [추가] 라운드 통계 업데이트 (인원수, 포인트 증가) - betRound.addBetStats(userBetRequest.getOption(), stake); - // 변경감지(Dirty Checking)에 의해 betRoundRepository.save(betRound) 없이도 저장되지만, - // 명시적으로 저장해도 무방. + // [삭제] 기존 엔티티 메서드 호출 방식 (동시성 문제 발생) + //betRound.addBetStats(userBetRequest.getOption(), stake); + + if (userBetRequest.getOption() == BetOption.RISE) { + betRoundRepository.incrementUpStats(betRound.getBetRoundID(), stake); + } else { + betRoundRepository.incrementDownStats(betRound.getBetRoundID(), stake); + } if (!userBetRequest.isFree()) { if (!userBetRequest.isStakePointsValid()) { From 4bdfaf504ea7c5ae5810513a9ac77afa03474101 Mon Sep 17 00:00:00 2001 From: 20011744 <141496725+cksdid202@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:27:52 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/betting/repository/BetRoundRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java b/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java index b9064fdd..2205e07c 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java @@ -1,6 +1,6 @@ package org.sejongisc.backend.betting.repository; -import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param; import org.sejongisc.backend.betting.entity.BetRound; import org.sejongisc.backend.betting.entity.Scope; import org.springframework.data.jpa.repository.JpaRepository; From 49ddcb45873886e7826877fa5907bbc4906f1e77 Mon Sep 17 00:00:00 2001 From: 20011744 <141496725+cksdid202@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:20:58 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20JSON=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../betting/controller/BettingController.java | 9 ++-- .../backend/betting/dto/UserBetResponse.java | 41 +++++++++++++++++++ .../backend/betting/entity/BetRound.java | 5 +++ .../betting/service/BettingService.java | 36 +++++++++++----- .../config/PrimaryDataSourceConfig.java | 1 + 5 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetResponse.java diff --git a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java index 4ff2c154..17de4597 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java @@ -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; @@ -91,12 +92,12 @@ public ResponseEntity> getAllBetRounds() { } ) @PostMapping("/user-bets") - public ResponseEntity postUserBet( + public ResponseEntity 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); } @@ -135,11 +136,11 @@ public ResponseEntity cancelUserBet( } ) @GetMapping("/user-bets/history") - public ResponseEntity> getAllUserBets( + public ResponseEntity> getAllUserBets( @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails principal) { - List userBets = bettingService.getAllMyBets(principal.getUserId()); + List userBets = bettingService.getAllMyBets(principal.getUserId()); return ResponseEntity.ok(userBets); } } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetResponse.java b/backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetResponse.java new file mode 100644 index 00000000..59f29870 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetResponse.java @@ -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(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java index bae72b2e..05a04e26 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java @@ -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 = "베팅이 열리는 시각 (유저 참여 시작 시점)") @@ -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; // 라운드가 현재 진행 중인지 여부 반환 diff --git a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java index cea5bf70..e8c35ab4 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java @@ -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; @@ -85,10 +86,16 @@ public boolean setAllowFree() { } /** - * 사용자의 전체 베팅 내역 조회 + * 사용자의 전체 베팅 내역 조회 (수정됨) */ - public List getAllMyBets(UUID userId) { - return userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId); + @Transactional(readOnly = true) // 트랜잭션 유지 필수 + public List getAllMyBets(UUID userId) { + List userBets = userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId); + + // Entity List -> DTO List 변환 + return userBets.stream() + .map(UserBetResponse::from) + .toList(); } /** @@ -128,30 +135,34 @@ 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); @@ -159,14 +170,14 @@ public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) { 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) @@ -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); } diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java index 4f85b0c7..e4326b52 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java @@ -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")