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