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
37 changes: 37 additions & 0 deletions src/main/java/dugout/DugOut/domain/LiveWinPrediction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dugout.DugOut.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


@Entity
@Table(name = "live_win_prediction")
@Getter
@Setter
@NoArgsConstructor
public class LiveWinPrediction {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

@Column(name = "game_id", nullable = false)
private String gameId;

@Column(name="inning", nullable = false)
private Integer inning;

@Column(name="win_probability")
private Double winProbability;

@Column(name="home_accum_score")
private Integer homeAccumScore;

@Column(name="away_accum_score")
private Integer awayAccumScore;


}
33 changes: 33 additions & 0 deletions src/main/java/dugout/DugOut/domain/enums/TeamCodeMapping.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dugout.DugOut.domain.enums;

import java.util.HashMap;
import java.util.Map;

public class TeamCodeMapping {

/**
* key: 2글자 약어 (ex: "SS", "LG", "LT", …)
* value: 팀 풀네임(한국어) (ex: "삼성", "LG", "롯데", …)
*/
public static final Map<String, String> ABBR_TO_FULL = new HashMap<>();

static {
ABBR_TO_FULL.put("SS", "삼성");
ABBR_TO_FULL.put("LG", "LG");
ABBR_TO_FULL.put("LT", "롯데");
ABBR_TO_FULL.put("HT", "KIA");
ABBR_TO_FULL.put("HH", "한화");
ABBR_TO_FULL.put("NC", "NC");
ABBR_TO_FULL.put("SK", "SSG");
ABBR_TO_FULL.put("OB", "두산");
ABBR_TO_FULL.put("KT", "KT");
ABBR_TO_FULL.put("WO", "키움");
}

/**
* 약어가 맵에 없을 때 반환할 기본값(Optional)
*/
public static String getFullName(String abbr) {
return ABBR_TO_FULL.getOrDefault(abbr, "UNKNOWN");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dugout.DugOut.repository;


import dugout.DugOut.domain.LiveWinPrediction;
import dugout.DugOut.web.dto.LiveWinPredictionDto;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Date;
import java.util.List;

@Repository
public interface LiveWinPredictionsRepository extends JpaRepository<LiveWinPrediction, Long> {

@Query(value =
"SELECT DATE(predicted_at) " +
"FROM live_win_predictions " +
"GROUP BY DATE(predicted_at) " +
"ORDER BY DATE(predicted_at) DESC " +
"LIMIT 1",
nativeQuery = true)
Date findLatestPredictionDate();

@Query(value =
"SELECT " +
" p.game_id AS gameId, " +
" p.inning AS inning, " +
" p.win_probability AS winProbability, " +
" p.home_accum_score AS homeAccumScore, " +
" p.away_accum_score AS awayAccumScore, " +
" p.predicted_at AS predictedAt " +
"FROM live_win_predictions p " +
"INNER JOIN ( " +
" SELECT " +
" game_id, " +
" MAX(predicted_at) AS max_ts " +
" FROM live_win_predictions " +
" WHERE DATE(predicted_at) = :latestDate " +
" GROUP BY game_id " +
") AS latest_per_game " +
" ON p.game_id = latest_per_game.game_id " +
" AND p.predicted_at = latest_per_game.max_ts " +
"WHERE DATE(p.predicted_at) = :latestDate " +
"ORDER BY p.game_id",
nativeQuery = true)
List<LiveWinPredictionDto> findLatestPredictionsByDate(
@Param("latestDate") Date latestDate
);
}
92 changes: 92 additions & 0 deletions src/main/java/dugout/DugOut/service/LiveWinPredictionService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package dugout.DugOut.service;

import dugout.DugOut.domain.enums.TeamCodeMapping;
import dugout.DugOut.repository.LiveWinPredictionsRepository;
import dugout.DugOut.web.dto.LiveWinPredictionDto;
import dugout.DugOut.web.dto.response.LiveWinPredictionResponse;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;


@Service
public class LiveWinPredictionService {

private final LiveWinPredictionsRepository repository;

public LiveWinPredictionService(LiveWinPredictionsRepository repository) {
this.repository = repository;
}

/**
* 1) 가장 최근 날짜 조회
* 2) 해당 날짜의 각 게임별 최신 예측 리스트 조회
* 3) gameId 뒤 4글자를 파싱해 원정/홈팀 약어 추출
* 4) 약어 → 풀네임 매핑
* 5) LiveWinPredictionResponse 객체로 만들어 반환
*/
public List<LiveWinPredictionResponse> getLatestPredictionsWithTeams() {
// 1) 가장 최근 날짜 조회
Date latestDate = repository.findLatestPredictionDate();
if (latestDate == null) {
// 데이터가 하나도 없으면 빈 리스트 반환
return Collections.emptyList();
}

// 2) 해당 날짜에 대한 각 게임별 마지막 예측 조회(DTO 형태)
List<LiveWinPredictionDto> dtos = repository.findLatestPredictionsByDate((java.sql.Date) latestDate);

// 3) DTO를 순회하면서 “gameId → awayTeam, homeTeam” 매핑 후 최종 응답 DTO로 변환
return dtos.stream()
.map(this::mapToResponseWithTeams)
.collect(Collectors.toList());
}

/**
* LiveWinPredictionDto에서 gameId를 파싱해
* - ‘뒤 4글자’를 두 덩어리(2글자 + 2글자)로 나눠서
* - 앞 2글자(원정팀 약어) / 뒤 2글자(홈팀 약어)로 분리
* - TeamCodeMapping을 통해 풀네임을 얻어
* LiveWinPredictionResponse 객체를 생성하여 반환
*/
private LiveWinPredictionResponse mapToResponseWithTeams(LiveWinPredictionDto dto) {
String gameId = dto.getGameId();
if (gameId == null || gameId.length() < 4) {
// gameId가 형식에 맞지 않으면 UNKNOWN 처리
return new LiveWinPredictionResponse(
gameId,
"UNKNOWN",
"UNKNOWN",
dto.getInning(),
dto.getWinProbability(),
dto.getHomeAccumScore(),
dto.getAwayAccumScore(),
dto.getPredictedAt()
);
}

// 예: gameId="20250604HTOB"
// 0123456789AB index → 뒤 4글자("HTOB") 기준: length-4 ~ length-2("HT"), length-2~length("OB")
int len = gameId.length();
String awayAbbr = gameId.substring(len - 4, len - 2); // e.g. "HT"
String homeAbbr = gameId.substring(len - 2, len); // e.g. "OB"

// TeamCodeMapping에서 풀네임 가져오기
String awayTeamFull = TeamCodeMapping.getFullName(awayAbbr);
String homeTeamFull = TeamCodeMapping.getFullName(homeAbbr);

return new LiveWinPredictionResponse(
gameId,
awayTeamFull,
homeTeamFull,
dto.getInning(),
dto.getWinProbability(),
dto.getHomeAccumScore(),
dto.getAwayAccumScore(),
dto.getPredictedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dugout.DugOut.web.controller;

import dugout.DugOut.service.LiveWinPredictionService;
import dugout.DugOut.service.WinProbabilityService;
import dugout.DugOut.web.dto.LiveWinPredictionDto;
import dugout.DugOut.web.dto.response.LiveWinPredictionResponse;
import dugout.DugOut.web.dto.response.WinProbabilityDto;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.format.annotation.DateTimeFormat;
Expand All @@ -17,9 +20,11 @@
@RequestMapping("/api/win-rates")
public class WinProbabilityController {
private final WinProbabilityService service;
private final LiveWinPredictionService liveWinPredictionService;

public WinProbabilityController(WinProbabilityService service) {
public WinProbabilityController(WinProbabilityService service, LiveWinPredictionService liveWinPredictionService) {
this.service = service;
this.liveWinPredictionService = liveWinPredictionService;
}


Expand All @@ -35,4 +40,11 @@ public ResponseEntity<List<WinProbabilityDto>> getByDate(
List<WinProbabilityDto> list = service.getWinRatesByDate(date);
return ResponseEntity.ok(list);
}

@Operation(summary = "실시간 예측값 반환")
@GetMapping("/live-prediction")
public ResponseEntity<List<LiveWinPredictionResponse>> getLatestPredictionsWithTeams() {
List<LiveWinPredictionResponse> results = liveWinPredictionService.getLatestPredictionsWithTeams();
return ResponseEntity.ok(results);
}
}
13 changes: 13 additions & 0 deletions src/main/java/dugout/DugOut/web/dto/LiveWinPredictionDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dugout.DugOut.web.dto;


import java.time.LocalDateTime;

public interface LiveWinPredictionDto {
String getGameId();
Integer getInning();
Float getWinProbability();
Integer getHomeAccumScore();
Integer getAwayAccumScore();
LocalDateTime getPredictedAt();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package dugout.DugOut.web.dto.response;

import lombok.Getter;

import java.time.LocalDateTime;


@Getter
public class LiveWinPredictionResponse {
private final String gameId;
private final String awayTeam;
private final String homeTeam;
private final Integer inning;
private final Float winProbability;
private final Integer homeAccumScore;
private final Integer awayAccumScore;
private final LocalDateTime predictedAt;

public LiveWinPredictionResponse(String gameId,
String awayTeam,
String homeTeam,
Integer inning,
Float winProbability,
Integer homeAccumScore,
Integer awayAccumScore,
LocalDateTime predictedAt) {
this.gameId = gameId;
this.awayTeam = awayTeam;
this.homeTeam = homeTeam;
this.inning = inning;
this.winProbability = winProbability;
this.homeAccumScore = homeAccumScore;
this.awayAccumScore = awayAccumScore;
this.predictedAt = predictedAt;
}
}