...
This commit is contained in:
parent
73072d1812
commit
93ff0354dc
@ -273,51 +273,59 @@ class PuzzleService(
|
||||
*/
|
||||
suspend fun sudoku_startGame(difficulty: String): SudokuGameDto {
|
||||
|
||||
// 🔽 [신규] 8단계 난이도를 (blockSize, generatorLevel)로 매핑
|
||||
// 🔽 [신규] 9단계 난이도를 (blockSize, generatorLevel)로 매핑
|
||||
val (blockSize, generatorLevel) = when (difficulty) {
|
||||
"1" -> Pair(2, 1) // Level 1: 4x4, Easy
|
||||
"2" -> Pair(2, 3) // Level 2: 4x4, Medium
|
||||
"3" -> Pair(3, 1) // Level 3: 9x9, Easy
|
||||
"4" -> Pair(3, 3) // Level 4: 9x9, Medium (Default)
|
||||
"5" -> Pair(3, 5) // Level 5: 9x9, Hard
|
||||
"6" -> Pair(4, 1) // Level 6: 16x16, Easy
|
||||
"7" -> Pair(4, 3) // Level 7: 16x16, Medium
|
||||
"8" -> Pair(4, 5) // Level 8: 16x16, Hard
|
||||
else -> Pair(3, 3) // 기본값 (Level 4)
|
||||
// 4x4 (3 levels)
|
||||
"1" -> Pair(2, 1) // L1: 4x4, Easy (Gen L1)
|
||||
"2" -> Pair(2, 3) // L2: 4x4, Medium (Gen L3)
|
||||
"3" -> Pair(2, 5) // L3: 4x4, Hard (Gen L5)
|
||||
|
||||
// 9x9 (5 levels)
|
||||
"4" -> Pair(3, 1) // L4: 9x9, Easy (Gen L1)
|
||||
"5" -> Pair(3, 2) // L5: 9x9, Medium (Gen L2)
|
||||
"6" -> Pair(3, 3) // L6: 9x9, Hard (Gen L3)
|
||||
"7" -> Pair(3, 4) // L7: 9x9, Expert (Gen L4)
|
||||
"8" -> Pair(3, 5) // L8: 9x9, Master (Gen L5)
|
||||
|
||||
// 16x16 (3 levels)
|
||||
"9" -> Pair(4, 1) // L9: 16x16, Easy (Gen L1)
|
||||
"10" -> Pair(4, 3) // L10: 16x16, Medium (Gen L3)
|
||||
"11" -> Pair(4, 5) // L11: 16x16, Hard (Gen L5)
|
||||
|
||||
else -> Pair(3, 3) // 기본값 (Level 6)
|
||||
}
|
||||
|
||||
// 1. DB에서 조건에 맞는 '미사용 문제' 검색
|
||||
val solutionPuzzle = sudokuPuzzleRepository
|
||||
.findFirstByBlockSizeAndLevelAndPlayCountOrderByPuzzleKey(blockSize, generatorLevel, 0L)
|
||||
// 2. 없으면 오류 반환 (스케줄러가 채워야 함)
|
||||
// 2. 없으면 오류 반환
|
||||
?: throw IllegalStateException("현재 $blockSize x $blockSize (Level $generatorLevel) 사용 가능한 새 퍼즐이 없습니다.")
|
||||
|
||||
// 3. 생성기 인스턴스화
|
||||
val generator = SudokuGenerator(blockSize)
|
||||
|
||||
// 4. DB의 정답(Solution)으로 문제(Question) 생성
|
||||
val puzzle = generator.generatePuzzleFromSolution(solutionPuzzle.solution!!, generatorLevel)
|
||||
|
||||
// 5. playCount 1 증가 및 저장
|
||||
// 3. playCount 1 증가 및 저장
|
||||
sudokuPuzzleRepository.save(
|
||||
solutionPuzzle.copy(playCount = solutionPuzzle.playCount + 1)
|
||||
)
|
||||
|
||||
// 6. DTO로 반환
|
||||
// 4. DTO로 반환 (DB에서 꺼낸 값 그대로)
|
||||
return SudokuGameDto(
|
||||
puzzleId = solutionPuzzle.puzzleKey ?: 0L,
|
||||
question = puzzle.question!!,
|
||||
solution = puzzle.solution!!,
|
||||
question = solutionPuzzle.question!!,
|
||||
solution = solutionPuzzle.solution!!,
|
||||
blockSize = solutionPuzzle.blockSize
|
||||
)
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 600000, initialDelay = 1000)
|
||||
suspend fun fillPuzzleCache() {
|
||||
// 🔽 [수정] 2..5 -> 2..4 (4x4까지만)
|
||||
// 🔽 [수정] 2..4 (4x4, 9x9, 16x16)
|
||||
for (blockSize in 2..4) {
|
||||
for (level in 1..5) { // 1~5 난이도
|
||||
// (이하 로직은 기존과 동일)
|
||||
// 🔽 [수정] 1, 3, 5 (Easy, Medium, Hard)
|
||||
for (level in 1..5) {
|
||||
|
||||
if ((blockSize == 2 || blockSize == 4) && (level == 2 || level == 4)) {
|
||||
continue
|
||||
}
|
||||
|
||||
val lockKey = "$blockSize-$level"
|
||||
if (generationLocks.putIfAbsent(lockKey, true) != null) {
|
||||
continue
|
||||
@ -359,10 +367,8 @@ class PuzzleService(
|
||||
* [수정] 이 함수는 이제 '새 정답 퍼즐'을 1개 생성하여
|
||||
* 'playCount = 0' 상태로 DB에 저장하고, '저장된 객체'를 반환합니다.
|
||||
*/
|
||||
suspend fun sudoku_generateAndSavePuzzle(blockSize: Int = 3, level: Int = 3) { // 👈 [수정]
|
||||
suspend fun sudoku_generateAndSavePuzzle(blockSize: Int = 3, level: Int = 3) {
|
||||
val generator = SudokuGenerator(blockSize)
|
||||
|
||||
// [수정] generatePuzzle은 Solution과 Question을 모두 반환
|
||||
val newPuzzle = generator.generatePuzzle(level)
|
||||
|
||||
val lastPuzzle = sudokuPuzzleRepository.findTopByOrderByPuzzleKeyDesc()
|
||||
@ -370,10 +376,10 @@ class PuzzleService(
|
||||
|
||||
val puzzleToSave = SudokuPuzzle(
|
||||
puzzleKey = nextKey,
|
||||
solution = newPuzzle.solution, // 👈 정답 저장
|
||||
question = newPuzzle.question, // 👈 문제 저장
|
||||
solution = newPuzzle.solution,
|
||||
question = newPuzzle.question,
|
||||
blockSize = blockSize,
|
||||
level = level, // 👈 레벨 저장
|
||||
level = level,
|
||||
playCount = 0L
|
||||
)
|
||||
sudokuPuzzleRepository.save(puzzleToSave)
|
||||
@ -583,13 +589,10 @@ data class SudokuPuzzle(
|
||||
@Id
|
||||
val id: String? = null,
|
||||
val puzzleKey: Long? = null,
|
||||
|
||||
// 🔽 [수정] 'solution' (정답)과 'question' (문제)을 모두 저장
|
||||
val solution: String?, // 완성된 퍼즐
|
||||
val question: String?, // 빈칸이 뚫린 퍼즐
|
||||
|
||||
val solution: String?,
|
||||
val question: String?, // 👈 [추가]
|
||||
val blockSize: Int = 3,
|
||||
val level: Int = 3, // 🔽 [추가] 이 퍼즐의 난이도 (1~5)
|
||||
val level: Int = 3, // 👈 [추가] 생성기 난이도 (1, 3, 5)
|
||||
val playCount: Long = 0L
|
||||
)
|
||||
|
||||
@ -610,14 +613,14 @@ interface SudokuPuzzleRepository : CoroutineCrudRepository<SudokuPuzzle, String>
|
||||
])
|
||||
suspend fun findRandomByBlockSize(blockSize: Int): SudokuPuzzle?
|
||||
|
||||
// 🔽 [수정] blockSize, level, playCount 모두 일치하는 퍼즐 검색
|
||||
// [수정] blockSize, level, playCount 모두 일치하는 퍼즐 검색
|
||||
suspend fun findFirstByBlockSizeAndLevelAndPlayCountOrderByPuzzleKey(
|
||||
blockSize: Int,
|
||||
level: Int,
|
||||
playCount: Long
|
||||
): SudokuPuzzle?
|
||||
|
||||
// 🔽 [추가] 스케줄러가 사용할, 특정 조건의 퍼즐 개수 카운트
|
||||
// [추가] 스케줄러가 사용할, 특정 조건의 퍼즐 개수 카운트
|
||||
suspend fun countByBlockSizeAndLevelAndPlayCount(
|
||||
blockSize: Int,
|
||||
level: Int,
|
||||
@ -720,90 +723,78 @@ class GameRankService(
|
||||
val auth = SecurityContextHolder.getContext().authentication
|
||||
val isAuthenticated = auth != null && auth.isAuthenticated && auth !is AnonymousAuthenticationToken
|
||||
|
||||
val gameRankMono: Mono<GameRank>
|
||||
|
||||
if (isAuthenticated) {
|
||||
// --- 1. 인증된 사용자 (로그인 상태) ---
|
||||
val principal = auth.principal as UserDetails
|
||||
val authenticatedUserId = principal.username
|
||||
gameRankMono = Mono.just(GameRank(
|
||||
val gameRank = GameRank(
|
||||
userId = authenticatedUserId,
|
||||
gameType = rankDto.gameType,
|
||||
contextId = rankDto.contextId,
|
||||
playerName = authenticatedUserId,
|
||||
playerName = authenticatedUserId, // 이름은 인증된 ID로 고정
|
||||
primaryScore = rankDto.primaryScore,
|
||||
secondaryScore = rankDto.secondaryScore
|
||||
))
|
||||
)
|
||||
// 로그인 유저는 중복 검사 없이 바로 저장
|
||||
return rankRepository.save(gameRank)
|
||||
|
||||
} else {
|
||||
// --- 2. 익명 사용자 (비로그인 상태) ---
|
||||
val anonymousUserId = rankDto.userId
|
||||
val requestedName = rankDto.playerName
|
||||
|
||||
// 🔽 [수정] 'checkAuthUsers'의 예외 처리를 더 견고하게 변경
|
||||
// [수정된 검증 로직]
|
||||
// 1. 이 이름이 '인증된(회원) 이름'인지 확인 (Blocking)
|
||||
val checkAuthUsers = Mono.fromCallable {
|
||||
userManager.loadUserByUsername(requestedName)
|
||||
}
|
||||
.subscribeOn(Schedulers.boundedElastic()) // 블로킹 IO 작업을 위한 스케줄러
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap<GameRank> {
|
||||
// loadUserByUsername이 UserDetails를 반환했다면 (예외가 안 났다면)
|
||||
// -> 이름이 중복된다는 뜻
|
||||
// 유저가 존재하면 -> 중복 오류
|
||||
Mono.error(IllegalArgumentException("이미 등록된 회원의 이름입니다."))
|
||||
}
|
||||
.onErrorResume { error ->
|
||||
// [수정] 모든 예외(error)를 잡아서 처리
|
||||
when (error) {
|
||||
is UsernameNotFoundException -> {
|
||||
// '사용자를 찾을 수 없음'은 정상적인 '중복 아님' 시나리오임
|
||||
.onErrorResume(UsernameNotFoundException::class.java) {
|
||||
// 유저가 존재하지 않으면 -> 통과
|
||||
Mono.empty()
|
||||
}
|
||||
is IllegalArgumentException -> {
|
||||
// 'flatMap'에서 우리가 직접 발생시킨 "이미 등록된 회원" 오류
|
||||
Mono.error(error)
|
||||
}
|
||||
else -> {
|
||||
// 그 외의 모든 서버 내부 크래시 (NPE, DB 오류 등)
|
||||
.onErrorResume { error ->
|
||||
// 그 외 NPE 등 모든 서버 오류
|
||||
logService.log("!!! submitRank: checkAuthUsers 중 예상치 못한 크래시 발생 !!!", error)
|
||||
Mono.error(IllegalArgumentException("이름 확인 중 서버 오류가 발생했습니다."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2b. 'rankRepository'는 'Reactive'이므로 Schedulers가 필요 없음 (기존과 동일)
|
||||
val checkAnonymousUsers = rankRepository.findFirstByUserId(anonymousUserId)
|
||||
.flatMap { existingRank ->
|
||||
if (existingRank.playerName != requestedName) {
|
||||
Mono.error(IllegalArgumentException("해당 ID는 이미 '${existingRank.playerName}' 님으로 등록되어 있습니다."))
|
||||
// 2. 이 이름이 '다른 익명 유저'의 이름인지 확인 (Reactive)
|
||||
val checkAnonymousUsers = rankRepository.findByPlayerName(requestedName)
|
||||
.next() // 이 이름을 가진 랭킹 '1개'만 찾음
|
||||
.flatMap<GameRank> { rankWithSameName ->
|
||||
// 랭킹이 존재하면
|
||||
if (rankWithSameName.userId == anonymousUserId) {
|
||||
// 그게 내 ID임 (예: "Bum"으로 등록 후, "Bum"으로 다시 등록)
|
||||
// -> 통과
|
||||
Mono.empty()
|
||||
} else {
|
||||
Mono.empty<GameRank>()
|
||||
}
|
||||
}
|
||||
.switchIfEmpty(Mono.defer {
|
||||
rankRepository.findByPlayerName(requestedName).hasElements()
|
||||
.flatMap { nameExists ->
|
||||
if (nameExists) {
|
||||
// 내 ID가 아님 (다른 사람이 "Bum" 사용 중)
|
||||
// -> 중복 오류
|
||||
Mono.error(IllegalArgumentException("이미 사용 중인 이름입니다."))
|
||||
} else {
|
||||
Mono.empty<GameRank>()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 2c. 모든 검증을 통과하면 랭킹 생성 (기존과 동일)
|
||||
gameRankMono = checkAuthUsers.then(checkAnonymousUsers)
|
||||
// 3. 모든 검증 통과 후 랭킹 생성
|
||||
val gameRankMono = checkAuthUsers.then(checkAnonymousUsers)
|
||||
.then(Mono.just(GameRank(
|
||||
userId = anonymousUserId,
|
||||
gameType = rankDto.gameType,
|
||||
contextId = rankDto.contextId,
|
||||
playerName = requestedName,
|
||||
playerName = requestedName, // 👈 검증된 이름
|
||||
primaryScore = rankDto.primaryScore,
|
||||
secondaryScore = rankDto.secondaryScore
|
||||
)))
|
||||
}
|
||||
|
||||
// 3. 랭킹 저장
|
||||
// 4. 랭킹 저장
|
||||
// [수정] 'findFirstByUserId'는 이제 필요 없으므로 'flatMap' 대신 'then' 사용
|
||||
return gameRankMono.flatMap { rankRepository.save(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 플레이어의 모든 게임 랭킹을 조회합니다. (변경 없음)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user