Add Maia chess gameplay and deployment
Some checks failed
Deploy Blog Backend / test (push) Successful in 2m6s
Deploy Blog Backend / deploy (push) Failing after 1m8s

This commit is contained in:
박원엽
2026-06-19 14:49:20 +09:00
parent b47258ea53
commit 3042dc898e
26 changed files with 1682 additions and 32 deletions

View File

@@ -0,0 +1,82 @@
package me.wypark.blogbackend.api.controller
import jakarta.validation.Valid
import me.wypark.blogbackend.api.common.ApiResponse
import me.wypark.blogbackend.api.dto.ChessGameCreateRequest
import me.wypark.blogbackend.api.dto.ChessGamePgnResponse
import me.wypark.blogbackend.api.dto.ChessGameResponse
import me.wypark.blogbackend.api.dto.ChessGameStatsResponse
import me.wypark.blogbackend.api.dto.ChessGameSummaryResponse
import me.wypark.blogbackend.api.dto.ChessMoveRequest
import me.wypark.blogbackend.domain.auth.CustomUserDetails
import me.wypark.blogbackend.domain.chess.ChessGameService
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/chess/games")
class ChessGameController(
private val chessGameService: ChessGameService
) {
@PostMapping
fun createGame(
@AuthenticationPrincipal userDetails: CustomUserDetails,
@Valid @RequestBody request: ChessGameCreateRequest
): ResponseEntity<ApiResponse<ChessGameResponse>> {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.success(chessGameService.createGame(userDetails.memberId, request)))
}
@GetMapping
fun getGames(
@AuthenticationPrincipal userDetails: CustomUserDetails,
@PageableDefault(size = 20, sort = ["updatedAt"], direction = Sort.Direction.DESC) pageable: Pageable
): ResponseEntity<ApiResponse<Page<ChessGameSummaryResponse>>> {
return ResponseEntity.ok(ApiResponse.success(chessGameService.getGames(userDetails.memberId, pageable)))
}
@GetMapping("/stats")
fun getStats(
@AuthenticationPrincipal userDetails: CustomUserDetails
): ResponseEntity<ApiResponse<ChessGameStatsResponse>> {
return ResponseEntity.ok(ApiResponse.success(chessGameService.getStats(userDetails.memberId)))
}
@GetMapping("/{gameId}")
fun getGame(
@AuthenticationPrincipal userDetails: CustomUserDetails,
@PathVariable gameId: String
): ResponseEntity<ApiResponse<ChessGameResponse>> {
return ResponseEntity.ok(ApiResponse.success(chessGameService.getGame(userDetails.memberId, gameId)))
}
@GetMapping("/{gameId}/pgn")
fun getPgn(
@AuthenticationPrincipal userDetails: CustomUserDetails,
@PathVariable gameId: String
): ResponseEntity<ApiResponse<ChessGamePgnResponse>> {
return ResponseEntity.ok(ApiResponse.success(chessGameService.getPgn(userDetails.memberId, gameId)))
}
@PostMapping("/{gameId}/moves")
fun playMove(
@AuthenticationPrincipal userDetails: CustomUserDetails,
@PathVariable gameId: String,
@Valid @RequestBody request: ChessMoveRequest
): ResponseEntity<ApiResponse<ChessGameResponse>> {
return ResponseEntity.ok(ApiResponse.success(chessGameService.playMove(userDetails.memberId, gameId, request)))
}
}

View File

@@ -0,0 +1,153 @@
package me.wypark.blogbackend.api.dto
import jakarta.validation.constraints.DecimalMax
import jakarta.validation.constraints.DecimalMin
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.Pattern
import me.wypark.blogbackend.domain.chess.ChessGameHistoryStats
import me.wypark.blogbackend.domain.chess.ChessGameRecord
import me.wypark.blogbackend.domain.chess.ChessGameSession
import java.time.LocalDateTime
data class ChessGameCreateRequest(
@field:Min(600, message = "레이팅은 600 이상이어야 합니다.")
@field:Max(2600, message = "레이팅은 2600 이하여야 합니다.")
val rating: Int = 1500,
@field:Pattern(
regexp = "(?i)^(white|black)$",
message = "playerColor는 white 또는 black이어야 합니다."
)
val playerColor: String = "white",
@field:Pattern(
regexp = "^(3m|5m|23m|79m)$",
message = "model은 3m, 5m, 23m, 79m 중 하나여야 합니다."
)
val model: String = "5m",
@field:DecimalMin(value = "0.0", message = "temperature는 0.0 이상이어야 합니다.")
@field:DecimalMax(value = "2.0", message = "temperature는 2.0 이하여야 합니다.")
val temperature: Double = 0.8,
@field:DecimalMin(value = "0.0", message = "topP는 0.0 이상이어야 합니다.")
@field:DecimalMax(value = "1.0", message = "topP는 1.0 이하여야 합니다.")
val topP: Double = 0.95
)
data class ChessMoveRequest(
@field:Pattern(
regexp = "^[a-h][1-8][a-h][1-8][qrbn]?$",
message = "move는 UCI 형식이어야 합니다. 예: e2e4, e7e8q"
)
val move: String
)
data class ChessGameResponse(
val gameId: String,
val rating: Int,
val playerColor: String,
val model: String,
val fen: String,
val turn: String,
val moves: List<String>,
val status: String,
val result: String?,
val outcome: String,
val pgn: String,
val maiaMove: String?
) {
companion object {
fun from(session: ChessGameSession, maiaMove: String? = null): ChessGameResponse {
return ChessGameResponse(
gameId = session.gameId,
rating = session.rating,
playerColor = session.playerColor.value,
model = session.model,
fen = session.fen,
turn = session.turn.value,
moves = session.moves,
status = session.status,
result = session.result,
outcome = session.outcome().name,
pgn = session.pgn,
maiaMove = maiaMove
)
}
fun from(record: ChessGameRecord, maiaMove: String? = null): ChessGameResponse {
return ChessGameResponse(
gameId = record.gameId,
rating = record.rating,
playerColor = record.playerColor.value,
model = record.model,
fen = record.fen,
turn = record.turn.value,
moves = record.moveList(),
status = record.status,
result = record.result,
outcome = record.outcome.name,
pgn = record.pgn,
maiaMove = maiaMove
)
}
}
}
data class ChessGameSummaryResponse(
val gameId: String,
val rating: Int,
val playerColor: String,
val model: String,
val status: String,
val result: String?,
val outcome: String,
val movesCount: Int,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime
) {
companion object {
fun from(record: ChessGameRecord): ChessGameSummaryResponse {
return ChessGameSummaryResponse(
gameId = record.gameId,
rating = record.rating,
playerColor = record.playerColor.value,
model = record.model,
status = record.status,
result = record.result,
outcome = record.outcome.name,
movesCount = record.moveList().size,
createdAt = record.createdAt,
updatedAt = record.updatedAt
)
}
}
}
data class ChessGamePgnResponse(
val gameId: String,
val pgn: String
)
data class ChessGameStatsResponse(
val total: Long,
val inProgress: Long,
val wins: Long,
val losses: Long,
val draws: Long,
val unknown: Long
) {
companion object {
fun from(stats: ChessGameHistoryStats): ChessGameStatsResponse {
return ChessGameStatsResponse(
total = stats.total,
inProgress = stats.inProgress,
wins = stats.wins,
losses = stats.losses,
draws = stats.draws,
unknown = stats.unknown
)
}
}
}