This commit is contained in:
lunaticbum 2025-11-11 14:38:30 +09:00
parent 73072d1812
commit 93ff0354dc

View File

@ -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) }
}
}
/**
* 특정 플레이어의 모든 게임 랭킹을 조회합니다. (변경 없음)