Skip to content

Sisc1 52 be 주식 베팅 게임 구현#98

Merged
Kosw6 merged 7 commits intomainfrom
SISC1-52-BE-주식-베팅-게임-구현
Nov 12, 2025

Hidden character warning

The head ref may contain hidden characters: "SISC1-52-BE-\uc8fc\uc2dd-\ubca0\ud305-\uac8c\uc784-\uad6c\ud604"
Merged

Sisc1 52 be 주식 베팅 게임 구현#98
Kosw6 merged 7 commits intomainfrom
SISC1-52-BE-주식-베팅-게임-구현

Conversation

@otter2023
Copy link
Member

@otter2023 otter2023 commented Nov 12, 2025

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 관리자용 베팅 라운드 관리 기능 추가 (일일/주간 개설, 종료, 정산)
    • 사용자 베팅 내역 조회 및 베팅 취소 기능 추가
  • 개선사항

    • API 문서화 강화로 베팅 API 명세 개선
    • 베팅 요청 유효성 검증 강화
    • 가격 데이터 기반 시스템으로 업그레이드
  • 제거됨

    • 기존 주식 정보 시스템 제거

@coderabbitai
Copy link

coderabbitai bot commented Nov 12, 2025

코드 리뷰 분석

Walkthrough

스톡 엔티티를 프라이스데이터로 대체하며 베팅 시스템을 리팩토링합니다. 관리자 컨트롤러 추가, OpenAPI 어노테이션 확대, 사용자 베팅 히스토리 및 정산 기능 강화, Scope 열거형 JSON 직렬화 지원을 포함합니다.

Changes

Cohort / File(s) 변경 요약
관리자 API 새로 추가
backend/src/main/java/org/sejongisc/backend/betting/controller/BettingAdminController.java
관리자 베팅 라운드 관리용 새로운 REST 컨트롤러 생성. 일일/주간 라운드 개설, 라운드 종료, 베팅 정산 4개 엔드포인트 추가 (/api/admin/bet-rounds 하위).
베팅 컨트롤러 OpenAPI 문서화 및 기능 확장
backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java
@Tag, @Operation, @Parameter 어노테이션 추가로 OpenAPI 문서화 강화. Scope 열거형 PathVariable 지원, 사용자 베팅 히스토리 조회/취소 엔드포인트 추가. 인증 컨텍스트 개선.
스톡 엔티티 제거
backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java, backend/src/main/java/org/sejongisc/backend/betting/repository/StockRepository.java
Stock 엔티티 및 StockRepository 삭제. 프라이스데이터 기반 시스템으로 전환.
가격 데이터 DTO 및 직렬화 지원
backend/src/main/java/org/sejongisc/backend/betting/dto/PriceResponse.java, backend/src/main/java/org/sejongisc/backend/betting/entity/Scope.java
PriceResponse DTO 새로 추가 (PriceData 매핑). Scope 열거형에 @JsonCreator/@jsonvalue 추가하여 JSON 직렬화/역직렬화 지원.
DTO 유효성 검증 강화
backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java
@Schema 어노테이션 추가, isStakePointsValid() @AssertTrue 검증 메서드 추가 (무료/포인트 베팅 규칙 검증).
엔티티 OpenAPI 메타데이터
backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java
BetRound 필드들에 @Schema 어노테이션 추가하여 API 문서화 메타데이터 제공. 기능 로직 변경 없음.
서비스 로직 마이그레이션
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java
Stock 기반 로직을 PriceData로 완전 대체. getPriceData() 메서드 추가, 라운드 생성/정산 흐름을 PriceData 데이터 사용으로 변경. 보상 계산값 업데이트 (2→10).
리포지토리 쿼리 확장
backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java
findFirstByTickerOrderByDateDesc(String ticker) 메서드 추가로 최신 가격 데이터 조회 지원.
테스트 파일 비활성화
backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java
BettingServiceTest 전체 내용 주석 처리로 비활성화 (원본 코드는 보존).

Sequence Diagram(s)

sequenceDiagram
    participant Admin as 관리자
    participant Controller as BettingAdminController
    participant Service as BettingService
    participant PriceRepo as PriceDataRepository
    participant BetRepo as BetRoundRepository
    
    Admin->>Controller: POST /api/admin/bet-rounds/daily/open
    Controller->>Service: createBetRound(Scope.DAILY)
    Service->>PriceRepo: 모든 PriceData 조회 후 랜덤 선택
    PriceRepo-->>Service: PriceData 반환
    Service->>Service: PriceData → PriceResponse 변환
    Service->>BetRepo: 새 BetRound 생성 및 저장
    BetRepo-->>Service: BetRound 저장 완료
    Service-->>Controller: 성공 메시지
    Controller-->>Admin: 200 OK (ResponseEntity<String>)
Loading
sequenceDiagram
    participant User as 사용자
    participant Controller as BettingController
    participant Service as BettingService
    participant PriceRepo as PriceDataRepository
    participant BetRepo as BetRoundRepository
    
    rect rgb(200, 220, 255)
    note over User,BetRepo: 사용자 베팅 흐름 (변경됨: Stock → PriceData)
    User->>Controller: POST /user-bets (UserBetRequest)
    Controller->>Service: postUserBet(principal, request)
    Service->>BetRepo: 베팅 라운드 조회
    alt 라운드 없음
        Service-->>Controller: BET_ROUND_NOT_FOUND
    else 라운드 존재
        Service->>PriceRepo: 라운드용 PriceData 확인
        Service->>BetRepo: UserBet 저장
        Service-->>Controller: UserBet 반환
    end
    Controller-->>User: 200 OK / 404 / 409
    end
    
    User->>Controller: GET /user-bets/history
    Controller->>Service: getAllUserBets(principal)
    Service->>BetRepo: 사용자 베팅 조회
    Service-->>Controller: UserBet 리스트
    Controller-->>User: 200 OK (베팅 히스토리)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

주의 필요 사항:

  • Stock → PriceData 마이그레이션: BettingService에서 Stock 기반 로직을 PriceData로 완전히 교체한 부분. 라운드 생성, 정산 로직에서 데이터 소스 변경 검증 필요.
  • 서비스 메서드 보상 계산값 변경: calculateReward() 반환값 2→10으로 변경됨. 비즈니스 요구사항 확인 필요.
  • 테스트 비활성화: BettingServiceTest 전체가 주석 처리되어 있음. 새로운 PriceData 로직에 대한 테스트 커버리지 재구축 필요.
  • 다중 엔드포인트 추가: BettingController에 히스토리, 취소 등 여러 신규 엔드포인트 추가. 각 엔드포인트별 예외 처리 및 응답 코드 검증.
  • 관리자 API 신규 추가: BettingAdminController의 4개 엔드포인트가 기존 서비스 메서드를 올바르게 호출하고 있는지 확인.
  • 열거형 JSON 직렬화: Scope에 @JsonCreator/@jsonvalue 추가로 인한 직렬화 동작 변화. 기존 클라이언트 호환성 확인.

Possibly related PRs

Suggested reviewers

  • discipline24
  • Kosw6

Poem

🐰 스톡을 버리고 가격 데이터로 뛰어든 이 변화!
관리자 도구는 반짝반짝, 유저 베팅은 더 똑똑해졌네.
OpenAPI 문서는 환한 조명처럼 밝아졌고,
JSON 직렬화도 스르르 풀렸어요.
여기 한 마리 토끼가 축하하며 깡충깡충! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '주식 베팅 게임 구현'은 변경사항의 핵심인 BettingAdminController 추가, BettingController 개선, Stock에서 PriceData로의 마이그레이션, 그리고 베팅 서비스 확장을 정확하게 요약합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SISC1-52-BE-주식-베팅-게임-구현

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Kosw6 Kosw6 merged commit 4c4ffdd into main Nov 12, 2025
1 check was pending
@Kosw6 Kosw6 deleted the SISC1-52-BE-주식-베팅-게임-구현 branch November 12, 2025 14:41
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java (1)

3-77: JPA 엔티티에 직접 @Schema 어노테이션을 추가하면 영속성 계층과 API 계층이 결합됩니다.

JPA 엔티티에 OpenAPI 어노테이션을 직접 추가하면 다음과 같은 문제가 발생합니다:

  • 데이터베이스 스키마 변경이 API 스펙에 직접 영향을 미칩니다
  • API 버저닝이 어려워집니다
  • 엔티티의 모든 필드가 API에 노출될 위험이 있습니다

관련 파일들을 확인한 결과, 컨트롤러에서 엔티티를 직접 반환하고 있는 것으로 보입니다. DTO(Data Transfer Object) 레이어를 도입하여 API 응답 전용 클래스를 만들고, 그곳에 @Schema 어노테이션을 추가하는 것을 권장합니다.

예시:

// BetRoundResponse.java (새 파일)
@Schema(description = "베팅 라운드 응답")
public class BetRoundResponse {
    @Schema(description = "베팅 라운드의 고유 식별자")
    private UUID betRoundID;
    
    @Schema(description = "라운드 단위")
    private Scope scope;
    
    // ... 필요한 필드만 포함
    
    public static BetRoundResponse from(BetRound betRound) {
        // 엔티티 -> DTO 변환 로직
    }
}
🧹 Nitpick comments (1)
backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java (1)

49-50: TODO 주석: status 필드를 Enum으로 변경하는 것을 고려해보세요.

현재 status 필드는 boolean 타입으로 단순히 열림/닫힘만 표현하고 있습니다. 향후 베팅 라운드가 더 많은 상태(예: 대기중, 진행중, 베팅마감, 정산중, 정산완료 등)를 가질 가능성이 있다면, 지금 Enum으로 변경하는 것이 나중에 리팩토링 비용을 줄일 수 있습니다.

Enum 기반 구현을 도와드릴까요?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9a79d7 and 2c4eca0.

📒 Files selected for processing (11)
  • backend/src/main/java/org/sejongisc/backend/betting/controller/BettingAdminController.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (2 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/dto/PriceResponse.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java (2 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java (3 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/entity/Scope.java (2 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java (0 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/repository/StockRepository.java (0 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (6 hunks)
  • backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java (1 hunks)
  • backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java (1 hunks)
💤 Files with no reviewable changes (2)
  • backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java
  • backend/src/main/java/org/sejongisc/backend/betting/repository/StockRepository.java
🧰 Additional context used
🧬 Code graph analysis (2)
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (1)
backend/src/main/java/org/sejongisc/backend/point/service/PointHistoryService.java (1)
  • Service (33-104)
backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (1)
backend/src/main/java/org/sejongisc/backend/auth/controller/EmailController.java (1)
  • Tag (15-74)
🔇 Additional comments (5)
backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java (1)

79-127: 메서드 문서화가 잘 되어 있습니다.

각 메서드에 한글 주석이 추가되어 코드 가독성이 향상되었습니다.

backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (4)

3-6: OpenAPI 문서화가 체계적으로 추가되었습니다.

@Tag, @Operation, @Parameter, @ApiResponse 어노테이션을 통해 API 문서가 명확하게 작성되었습니다. 개발자와 사용자 모두에게 유용한 문서를 제공합니다.

Also applies to: 29-29


50-60: 타입 안정성 개선: String 대신 Scope enum 사용.

@PathVariable의 타입을 String에서 Scope enum으로 변경하여 컴파일 타임에 타입 안정성이 보장됩니다. Spring이 자동으로 문자열을 enum으로 변환하고, 잘못된 값이 입력되면 400 에러를 반환합니다.


34-48: 상세한 API 문서가 작성되었습니다.

각 엔드포인트마다 한글로 명확한 설명과 함께 가능한 응답 코드가 문서화되어 있습니다. 특히 내부 로직과 제약사항까지 설명되어 있어 API 사용자에게 매우 유용합니다.

Also applies to: 63-72, 79-92, 103-115, 127-136


95-96: @Parameter(hidden = true) 사용이 적절합니다.

@AuthenticationPrincipal로 주입되는 인증 정보는 Swagger UI에 표시할 필요가 없으므로, @Parameter(hidden = true)를 추가하여 API 문서를 깔끔하게 유지한 것이 좋습니다.

Also applies to: 118-119, 139-140

Comment on lines 50 to 60
@GetMapping("/bet-rounds/{scope}")
public ResponseEntity<BetRound> getTodayBetRound(
@PathVariable @Pattern(regexp = "daily|weekly") String scope){

Scope scopeEnum = Scope.valueOf(scope.toUpperCase());

Optional<BetRound> betRound = bettingService.getActiveRound(scopeEnum);
@Parameter(description = "라운드 범위 (Scope): DAILY 또는 WEEKLY", example = "DAILY")
@PathVariable Scope scope
) {
Optional<BetRound> betRound = bettingService.getActiveRound(scope);

return betRound
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

컨트롤러에서 엔티티 객체를 직접 반환하고 있습니다.

현재 모든 엔드포인트에서 BetRoundUserBet 엔티티를 직접 반환하고 있습니다. 이는 다음과 같은 문제를 야기합니다:

  1. 영속성 계층과 API 계층의 결합: 엔티티 구조 변경 시 API 스펙이 자동으로 변경됩니다
  2. 보안 위험: 의도하지 않은 필드(예: 내부 상태, 민감 정보)가 노출될 수 있습니다
  3. 순환 참조 위험: JPA 관계(@onetomany, @manytoone)가 있을 경우 JSON 직렬화 시 무한 루프 발생 가능
  4. API 버전 관리 어려움: 엔티티 변경 없이 API만 변경하기 어렵습니다

DTO 패턴을 도입하여 각 엔드포인트에 맞는 응답 클래스를 만드는 것을 강력히 권장합니다.

다음 스크립트로 엔티티에 JPA 관계 어노테이션이 있는지 확인하여 순환 참조 위험을 검증합니다:

Also applies to: 74-77, 94-101, 117-125, 138-144


🏁 Script executed:

#!/bin/bash
# Description: Check for JPA relationship annotations in entities that could cause circular reference issues

# Search for JPA relationship annotations in entity classes
rg -n --type java '@(OneToMany|ManyToOne|OneToOne|ManyToMany)' backend/src/main/java/org/sejongisc/backend/betting/entity/

Length of output: 131


추가 검증이 필요합니다. BetRound와 다른 엔티티의 양방향 관계를 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check for @OneToMany in BetRound entity and see relationship configuration
fd -e java --path backend/src/main/java/org/sejongisc/backend/betting/entity -exec grep -l "BetRound\|UserBet" {} \;

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Check for all @OneToMany and bidirectional relationships
rg -n '@OneToMany' backend/src/main/java/org/sejongisc/backend/betting/entity/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if serialization control annotations exist
rg -n '@(JsonIgnore|JsonBackReference|JsonManagedReference)' backend/src/main/java/org/sejongisc/backend/betting/entity/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Show UserBet.java full context around the @ManyToOne annotation
cat backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java | head -40

Length of output: 1118



---

🏁 Script executed:

```shell
#!/bin/bash
# Find and show BetRound entity
find backend/src/main/java/org/sejongisc/backend/betting/entity -name "BetRound.java" -exec cat {} \;

Length of output: 3523


🏁 Script executed:

#!/bin/bash
# Show the full BettingController to see all endpoints mentioned in review
cat backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java

Length of output: 5591


🏁 Script executed:

#!/bin/bash
# Check for any Jackson configuration that might control serialization
rg -n '@JsonInclude|@JsonProperty|@JsonView|@JsonSerialize' backend/src/main/java/org/sejongisc/backend/betting/ -A2

Length of output: 291


엔티티 객체의 직접 반환에 따른 아키텍처 위험 개선 필요

현재 모든 엔드포인트에서 BetRoundUserBet 엔티티를 직접 반환하고 있습니다. 순환 참조 위험은 현재 설정(unidirectional relationship + LAZY fetch)에서 즉각적인 문제는 아니지만, 다음의 아키텍처적 문제가 존재합니다:

  1. 영속성 계층과 API 계층의 결합: 엔티티 구조 변경 시 API 스펙이 자동으로 변경됩니다
  2. 보안 위험: 의도하지 않은 필드(예: 내부 상태, 민감 정보)가 노출될 수 있습니다
  3. API 버전 관리 어려움: 엔티티 변경 없이 API만 변경하기 어렵습니다
  4. 직렬화 동작의 불확실성: 미래 fetch 전략 변경 시 예상치 못한 동작 발생 가능

각 엔드포인트에 맞는 DTO 클래스를 작성하여 응답 데이터를 제어하기를 권장합니다 (50-60, 74-77, 94-101, 138-144 라인).

🤖 Prompt for AI Agents
backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java
(around lines 50-60, and also adjust endpoints at 74-77, 94-101, 138-144):
currently the controller returns JPA entities (BetRound/UserBet) directly;
replace those with dedicated response DTOs to decouple API from persistence.
Create BetRoundDto and UserBetDto containing only the fields the API should
expose, implement simple mapping from entity-to-DTO (in service or controller)
or use a mapper (MapStruct/constructors), change method signatures to return
ResponseEntity<BetRoundDto> / ResponseEntity<List<UserBetDto>> (or
Optional-wrapped DTO) and map Optional<Entity> to Optional<Dto> before building
the ResponseEntity, and ensure no lazy/internal/sensitive fields are exposed;
update all referenced endpoints at the listed line ranges to use the DTOs and
mapping.

Comment on lines +53 to 101
public PriceResponse getPriceData() {
List<PriceData> priceList = priceDataRepository.findAll();
if (priceList.isEmpty()) {
throw new CustomException(ErrorCode.STOCK_NOT_FOUND);
}
// TODO : 가중치 랜덤설정
return stocks.get(random.nextInt(stocks.size()));

PriceData price = priceList.get(random.nextInt(priceList.size()));

return PriceResponse.builder()
.name(price.getTicker())
.symbol(price.getTicker())
.market(MarketType.US)
.previousClosePrice(price.getClosePrice())
.settleClosePrice(price.getAdjustedClose())
.build();
}

/**
* 무료 베팅 가능 여부 (20% 확률)
*/
public boolean setAllowFree() {
return random.nextDouble() < 0.2;
}

/**
* 사용자의 전체 베팅 내역 조회
*/
public List<UserBet> getAllMyBets(UUID userId) {
// TODO : 필요 시 필터링, 정렬, 검색 로직 추가
return userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId);
}

/**
* 새로운 베팅 라운드 생성
*/
public void createBetRound(Scope scope) {

LocalDateTime now = LocalDateTime.now();

Stock stock = getStock();
PriceResponse price = getPriceData();

BetRound betRound = BetRound.builder()
.scope(scope)
.title(now.toLocalDate() + " " + stock.getName() + " " + scope.name() + " 라운드")
.symbol(stock.getSymbol())
.title(now.toLocalDate() + " " + price.getName() + " " + scope.name() + " 라운드")
.symbol(price.getSymbol())
.allowFree(setAllowFree())
.openAt(scope.getOpenAt(now))
.lockAt(scope.getLockAt(now))
.market(stock.getMarket())
.previousClosePrice(stock.getPreviousClosePrice())
.market(price.getMarket())
.previousClosePrice(price.getPreviousClosePrice())
.build();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

무작위 PriceData 선택이 최신 시세와 불일치합니다

getPriceData()priceDataRepository.findAll() 결과에서 임의로 한 건을 뽑는데, PriceData는 (ticker, date) 기반 시계열이라 과거 날짜 레코드를 선택할 수 있습니다. 이후 정산에서는 findFirstByTickerOrderByDateDesc로 최신 종가를 사용하므로, 라운드 생성 시 저장한 기준가와 정산 시점 가격이 전혀 다른 날짜에서 나온 값이 됩니다. 이는 베팅 결과를 왜곡시키는 치명적인 오류입니다.

무작위로 티커만 선택하고, 그 티커의 최신 데이터를 findFirstByTickerOrderByDateDesc로 조회하거나, 아예 레포지토리에서 “각 티커의 최신 레코드”만 가져오는 쿼리로 교체해 주세요.

🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java
around lines 53-101, getPriceData() currently picks a random PriceData from
priceDataRepository.findAll(), which can return old dated records and cause
mismatch with settlement logic; change it so you first pick a random ticker and
then load that ticker's latest record via
priceDataRepository.findFirstByTickerOrderByDateDesc(ticker) (or replace
findAll() with a repository query that returns only the latest record per
ticker), and ensure you handle the empty/absent case by throwing the existing
CustomException(ErrorCode.STOCK_NOT_FOUND).

Comment on lines +1 to +474
//package org.sejongisc.backend.betting.service;
//
//import org.junit.jupiter.api.BeforeEach;
//import org.junit.jupiter.api.DisplayName;
//import org.junit.jupiter.api.Test;
//import org.mockito.ArgumentCaptor;
//import org.sejongisc.backend.betting.dto.UserBetRequest;
//import org.sejongisc.backend.betting.entity.*;
//import org.sejongisc.backend.betting.repository.BetRoundRepository;
//import org.sejongisc.backend.betting.repository.StockRepository;
//import org.sejongisc.backend.betting.repository.UserBetRepository;
//import org.sejongisc.backend.common.exception.CustomException;
//import org.sejongisc.backend.common.exception.ErrorCode;
//import org.sejongisc.backend.point.entity.PointOrigin;
//import org.sejongisc.backend.point.entity.PointReason;
//import org.sejongisc.backend.point.service.PointHistoryService;
//
//import java.math.BigDecimal;
//import java.time.LocalDateTime;
//import java.util.*;
//
//import static org.assertj.core.api.Assertions.assertThat;
//import static org.junit.jupiter.api.Assertions.assertThrows;
//import static org.mockito.Mockito.*;
//
//class BettingServiceTest {
//
// private BetRoundRepository betRoundRepository;
// private StockRepository stockRepository;
// private UserBetRepository userBetRepository;
// private PointHistoryService pointHistoryService;
//
// private BettingService bettingService;
//
// private UUID userId;
// private UUID roundId;
//
// @BeforeEach
// void setUp() {
// betRoundRepository = mock(BetRoundRepository.class);
// stockRepository = mock(StockRepository.class);
// userBetRepository = mock(UserBetRepository.class);
// pointHistoryService = mock(PointHistoryService.class);
//
// bettingService = new BettingService(
// betRoundRepository,
// stockRepository,
// userBetRepository,
// pointHistoryService
// );
//
// userId = UUID.randomUUID();
// roundId = UUID.randomUUID();
// }
//
// // ---------------------
// // Stock / util tests
// // ---------------------
// @Test
// void getStock_빈리스트면_예외발생() {
// when(stockRepository.findAll()).thenReturn(List.of());
//
// CustomException ex = assertThrows(CustomException.class,
// () -> bettingService.getStock());
//
// assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.STOCK_NOT_FOUND);
// verify(stockRepository, times(1)).findAll();
// }
//
// @Test
// void getStock_리스트있으면_하나반환() {
// Stock s1 = Stock.builder()
// .name("삼성전자")
// .symbol("005930")
// .market(MarketType.KOREA)
// .previousClosePrice(BigDecimal.valueOf(85400))
// .build();
//
// Stock s2 = Stock.builder()
// .name("애플")
// .symbol("AAPL")
// .market(MarketType.US)
// .previousClosePrice(BigDecimal.valueOf(251.65))
// .build();
//
// when(stockRepository.findAll()).thenReturn(List.of(s1, s2));
//
// Stock chosen = bettingService.getStock();
//
// assertThat(List.of(s1, s2)).contains(chosen);
// verify(stockRepository, times(1)).findAll();
// }
//
// @Test
// void setAllowFree_항상_불린값반환() {
// boolean result = bettingService.setAllowFree();
// assertThat(result).isInstanceOf(Boolean.class);
// }
//
// // ---------------------
// // 조회 관련 테스트
// // ---------------------
// @Test
// @DisplayName("createBetRound_DAILY_정상저장")
// void createBetRound_DAILY_정상저장() {
// // given
// Stock stock = Stock.builder()
// .name("삼성전자")
// .symbol("005930")
// .market(MarketType.KOREA)
// .previousClosePrice(BigDecimal.valueOf(85400))
// .build();
//
// when(stockRepository.findAll()).thenReturn(List.of(stock));
//
// // when
// bettingService.createBetRound(Scope.DAILY);
//
// // then
// ArgumentCaptor<BetRound> captor = ArgumentCaptor.forClass(BetRound.class);
// verify(betRoundRepository, times(1)).save(captor.capture());
// BetRound saved = captor.getValue();
//
// assertThat(saved.getScope()).isEqualTo(Scope.DAILY);
// assertThat(saved.getSymbol()).isEqualTo("005930");
// assertThat(saved.getTitle()).contains("삼성전자");
// assertThat(saved.isOpen()).isTrue(); // 스케줄러에서 open() 호출 후 저장이라면 true여야 함
// }
//
// @Test
// @DisplayName("활성화된 DAILY BetRound 조회 성공")
// void findActiveRound_Success() {
// BetRound dailyRound = BetRound.builder()
// .betRoundID(UUID.randomUUID())
// .scope(Scope.DAILY)
// .status(true)
// .title("Daily Round")
// .openAt(LocalDateTime.now())
// .build();
//
// when(betRoundRepository.findByStatusTrueAndScope(Scope.DAILY))
// .thenReturn(Optional.of(dailyRound));
//
// Optional<BetRound> result = bettingService.getActiveRound(Scope.DAILY);
//
// assertThat(result).isPresent();
// assertThat(result.get().getScope()).isEqualTo(Scope.DAILY);
// assertThat(result.get().isOpen()).isTrue();
// verify(betRoundRepository, times(1)).findByStatusTrueAndScope(Scope.DAILY);
// }
//
// @Test
// @DisplayName("활성화된 BetRound가 없을 때 빈 Optional 반환")
// void findActiveRound_NotFound() {
// when(betRoundRepository.findByStatusTrueAndScope(Scope.DAILY))
// .thenReturn(Optional.empty());
//
// Optional<BetRound> result = bettingService.getActiveRound(Scope.DAILY);
//
// assertThat(result).isEmpty();
// verify(betRoundRepository, times(1)).findByStatusTrueAndScope(Scope.DAILY);
// }
//
// @Test
// @DisplayName("모든 BetRound 최신순 조회 성공")
// void getAllBetRounds_Success() {
// List<BetRound> betRounds = List.of(
// BetRound.builder()
// .betRoundID(UUID.randomUUID())
// .scope(Scope.DAILY)
// .status(true)
// .title("Daily Round")
// .openAt(LocalDateTime.now())
// .build(),
// BetRound.builder()
// .betRoundID(UUID.randomUUID())
// .scope(Scope.WEEKLY)
// .status(false)
// .title("Weekly Round")
// .openAt(LocalDateTime.now().minusDays(1))
// .build()
// );
//
// when(betRoundRepository.findAllByOrderBySettleAtDesc())
// .thenReturn(betRounds);
//
// List<BetRound> results = bettingService.getAllBetRounds();
//
// assertThat(results).hasSize(2);
// assertThat(results.get(0).getScope()).isEqualTo(Scope.DAILY);
// assertThat(results.get(1).getScope()).isEqualTo(Scope.WEEKLY);
// verify(betRoundRepository, times(1)).findAllByOrderBySettleAtDesc();
// }
//
// @Test
// @DisplayName("BetRound가 없을 때 빈 리스트 반환")
// void getAllBetRounds_Empty() {
// when(betRoundRepository.findAllByOrderBySettleAtDesc())
// .thenReturn(Collections.emptyList());
//
// List<BetRound> results = bettingService.getAllBetRounds();
//
// assertThat(results).isEmpty();
// verify(betRoundRepository, times(1)).findAllByOrderBySettleAtDesc();
// }
//
// @Test
// @DisplayName("WEEKLY BetRound 조회 성공")
// void findActiveRound_Weekly_Success() {
// BetRound weeklyRound = BetRound.builder()
// .betRoundID(UUID.randomUUID())
// .scope(Scope.WEEKLY)
// .status(true)
// .title("Weekly Round")
// .openAt(LocalDateTime.now())
// .build();
//
// when(betRoundRepository.findByStatusTrueAndScope(Scope.WEEKLY))
// .thenReturn(Optional.of(weeklyRound));
//
// Optional<BetRound> result = bettingService.getActiveRound(Scope.WEEKLY);
//
// assertThat(result).isPresent();
// assertThat(result.get().getScope()).isEqualTo(Scope.WEEKLY);
// verify(betRoundRepository, times(1)).findByStatusTrueAndScope(Scope.WEEKLY);
// }
//
// @Test
// @DisplayName("getAllMyBets() - 유저 ID로 조회 시 Repository 호출 및 결과 반환 확인")
// void getAllMyBets_Success() {
// UUID u = UUID.randomUUID();
// BetRound round = BetRound.builder()
// .title("테스트 라운드")
// .openAt(LocalDateTime.now().minusHours(2))
// .lockAt(LocalDateTime.now().plusHours(1))
// .settleAt(LocalDateTime.now().plusHours(2))
// .build();
//
// UserBet bet1 = UserBet.builder()
// .userBetId(UUID.randomUUID())
// .round(round)
// .userId(u)
// .option(BetOption.RISE)
// .stakePoints(100)
// .isFree(false)
// .build();
//
// UserBet bet2 = UserBet.builder()
// .userBetId(UUID.randomUUID())
// .round(round)
// .userId(u)
// .option(BetOption.FALL)
// .stakePoints(50)
// .isFree(true)
// .build();
//
// List<UserBet> mockResult = List.of(bet1, bet2);
// when(userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(u))
// .thenReturn(mockResult);
//
// List<UserBet> result = bettingService.getAllMyBets(u);
//
// verify(userBetRepository, times(1))
// .findAllByUserIdOrderByRound_SettleAtDesc(u);
// assertThat(result).hasSize(2);
// assertThat(result.get(0).getUserId()).isEqualTo(u);
// assertThat(result.get(1).getRound().getTitle()).isEqualTo("테스트 라운드");
// }
//
// // ---------------------
// // Bet creation / posting tests
// // ---------------------
// private BetRound openRoundNow() {
// LocalDateTime now = LocalDateTime.now();
// return BetRound.builder()
// .betRoundID(roundId)
// .scope(Scope.DAILY)
// .status(true)
// .title("OPEN")
// .openAt(now.minusMinutes(1))
// .lockAt(now.plusMinutes(10))
// .build();
// }
//
// private BetRound closedRoundNow() {
// LocalDateTime now = LocalDateTime.now();
// return BetRound.builder()
// .betRoundID(roundId)
// .scope(Scope.DAILY)
// .status(false)
// .title("CLOSED")
// .openAt(now.minusMinutes(10))
// .lockAt(now.minusMinutes(1))
// .build();
// }
//
// private UserBetRequest paidReq(int stake) {
// return UserBetRequest.builder()
// .roundId(roundId)
// .option(BetOption.RISE)
// .free(false)
// .stakePoints(stake)
// .build();
// }
//
// private UserBetRequest freeReq() {
// return UserBetRequest.builder()
// .roundId(roundId)
// .option(BetOption.FALL)
// .free(true)
// .stakePoints(999)
// .build();
// }
//
// @Test
// @DisplayName("postUserBet_paid_success")
// void postUserBet_paid_success() {
// BetRound round = openRoundNow();
//
// when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round));
// when(userBetRepository.existsByRoundAndUserId(round, userId)).thenReturn(false);
// when(userBetRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
//
// UserBetRequest req = paidReq(100);
//
// UserBet result = bettingService.postUserBet(userId, req);
//
// assertThat(result.getStakePoints()).isEqualTo(100);
// assertThat(result.isFree()).isFalse();
//
// verify(pointHistoryService).createPointHistory(
// eq(userId), eq(-100),
// eq(PointReason.BETTING),
// eq(PointOrigin.BETTING),
// eq(roundId)
// );
// verify(userBetRepository).save(any(UserBet.class));
// }
//
// @Test
// @DisplayName("postUserBet_free_success")
// void postUserBet_free_success() {
// BetRound round = openRoundNow();
//
// when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round));
// when(userBetRepository.existsByRoundAndUserId(round, userId)).thenReturn(false);
// when(userBetRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
//
// UserBetRequest req = freeReq();
//
// UserBet result = bettingService.postUserBet(userId, req);
//
// assertThat(result.isFree()).isTrue();
// assertThat(result.getStakePoints()).isZero();
//
// verify(pointHistoryService, never()).createPointHistory(any(), anyInt(), any(), any(), any());
// verify(userBetRepository).save(any(UserBet.class));
// }
//
// @Test
// @DisplayName("postUserBet_round_not_found")
// void postUserBet_round_not_found() {
// when(betRoundRepository.findById(roundId)).thenReturn(Optional.empty());
//
// CustomException ex = assertThrows(CustomException.class,
// () -> bettingService.postUserBet(userId, paidReq(100)));
//
// assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BET_ROUND_NOT_FOUND);
// verifyNoInteractions(pointHistoryService);
// }
//
// @Test
// @DisplayName("postUserBet_duplicate")
// void postUserBet_duplicate() {
// BetRound round = openRoundNow();
// when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round));
// when(userBetRepository.existsByRoundAndUserId(round, userId)).thenReturn(true);
//
// CustomException ex = assertThrows(CustomException.class,
// () -> bettingService.postUserBet(userId, paidReq(100)));
//
// assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BET_DUPLICATE);
// verifyNoInteractions(pointHistoryService);
// verify(userBetRepository, never()).save(any());
// }
//
// @Test
// @DisplayName("postUserBet_time_invalid")
// void postUserBet_time_invalid() {
// BetRound closed = closedRoundNow();
// when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(closed));
// when(userBetRepository.existsByRoundAndUserId(closed, userId)).thenReturn(false);
//
// CustomException ex = assertThrows(CustomException.class,
// () -> bettingService.postUserBet(userId, paidReq(100)));
//
// assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BET_ROUND_CLOSED);
// verifyNoInteractions(pointHistoryService);
// verify(userBetRepository, never()).save(any());
// }
//
// // ---------------------
// // cancelUserBet tests
// // ---------------------
// @Test
// @DisplayName("cancelUserBet_paid_refund_and_delete")
// void cancelUserBet_paid_refund_and_delete() {
// BetRound round = openRoundNow();
// UUID userBetId = UUID.randomUUID();
//
// UserBet bet = UserBet.builder()
// .userBetId(userBetId)
// .round(round)
// .userId(userId)
// .isFree(false)
// .stakePoints(200)
// .betStatus(BetStatus.ACTIVE)
// .build();
//
// when(userBetRepository.findByUserBetIdAndUserId(userBetId, userId))
// .thenReturn(Optional.of(bet));
//
// bettingService.cancelUserBet(userId, userBetId);
//
// verify(pointHistoryService).createPointHistory(
// eq(userId), eq(200),
// eq(PointReason.BETTING),
// eq(PointOrigin.BETTING),
// eq(userBetId)
// );
// verify(userBetRepository).delete(bet);
// }
//
// @Test
// @DisplayName("cancelUserBet_not_found")
// void cancelUserBet_not_found() {
// UUID userBetId = UUID.randomUUID();
// when(userBetRepository.findByUserBetIdAndUserId(userBetId, userId))
// .thenReturn(Optional.empty());
//
// CustomException ex = assertThrows(CustomException.class,
// () -> bettingService.cancelUserBet(userId, userBetId));
//
// assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BET_NOT_FOUND);
// verifyNoInteractions(pointHistoryService);
// verify(userBetRepository, never()).delete(any());
// }
//
// @Test
// @DisplayName("cancelUserBet_after_lock")
// void cancelUserBet_after_lock() {
// BetRound closed = closedRoundNow();
// UUID userBetId = UUID.randomUUID();
//
// UserBet bet = UserBet.builder()
// .userBetId(userBetId)
// .round(closed)
// .userId(userId)
// .isFree(false)
// .stakePoints(200)
// .betStatus(BetStatus.ACTIVE)
// .build();
//
// when(userBetRepository.findByUserBetIdAndUserId(userBetId, userId))
// .thenReturn(Optional.of(bet));
//
// CustomException ex = assertThrows(CustomException.class,
// () -> bettingService.cancelUserBet(userId, userBetId));
//
// assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BET_ROUND_CLOSED);
// verify(pointHistoryService, never()).createPointHistory(any(), anyInt(), any(), any(), any());
// verify(userBetRepository, never()).delete(any());
// }
//}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

테스트 클래스 전체 주석 처리로 인한 회귀 위험

BettingServiceTest의 모든 코드가 주석 처리되어 테스트가 전혀 실행되지 않습니다. 이 상태로는 이번 리팩토링에서 발생할 수 있는 회귀나 정산 로직 변경에 대한 검증이 불가능하며, 이후 수정 시 실패를 조기에 발견할 수 없습니다. 기존 테스트를 현행 로직(PriceData 기반 흐름)에 맞게 업데이트하거나 최소한 대체 검증을 제공해주세요. 주석 제거 없이 머지하는 것은 피해야 합니다.

🤖 Prompt for AI Agents
In
backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java
around lines 1-474, the entire test class is commented out causing no unit tests
to run; restore test coverage by uncommenting the class and its tests, update
the tests to match the current BettingService behavior (adjust setup, mocks, and
assertions to the new PriceData-based flow or any changed method signatures),
reintroduce/verifying necessary mocks for repositories and PointHistoryService,
and run the test suite (mvn test or equivalent) fixing any failing assertions or
compilation errors until all tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants