From 93ff0354dc131c380e5709bfac857ac682d53f2b Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 11 Nov 2025 14:38:30 +0900 Subject: [PATCH] ... --- .../lunaticbum/back/lun/model/PuzzleData.kt | 161 +++++++++--------- 1 file changed, 76 insertions(+), 85 deletions(-) diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt index c7d4f14..8b1d2f1 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/PuzzleData.kt @@ -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 ]) 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,89 +723,77 @@ class GameRankService( val auth = SecurityContextHolder.getContext().authentication val isAuthenticated = auth != null && auth.isAuthenticated && auth !is AnonymousAuthenticationToken - val gameRankMono: Mono - 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 { - // loadUserByUsername์ด UserDetails๋ฅผ ๋ฐ˜ํ™˜ํ–ˆ๋‹ค๋ฉด (์˜ˆ์™ธ๊ฐ€ ์•ˆ ๋‚ฌ๋‹ค๋ฉด) - // -> ์ด๋ฆ„์ด ์ค‘๋ณต๋œ๋‹ค๋Š” ๋œป + // ์œ ์ €๊ฐ€ ์กด์žฌํ•˜๋ฉด -> ์ค‘๋ณต ์˜ค๋ฅ˜ Mono.error(IllegalArgumentException("์ด๋ฏธ ๋“ฑ๋ก๋œ ํšŒ์›์˜ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.")) } + .onErrorResume(UsernameNotFoundException::class.java) { + // ์œ ์ €๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด -> ํ†ต๊ณผ + Mono.empty() + } .onErrorResume { error -> - // [์ˆ˜์ •] ๋ชจ๋“  ์˜ˆ์™ธ(error)๋ฅผ ์žก์•„์„œ ์ฒ˜๋ฆฌ - when (error) { - is UsernameNotFoundException -> { - // '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ'์€ ์ •์ƒ์ ์ธ '์ค‘๋ณต ์•„๋‹˜' ์‹œ๋‚˜๋ฆฌ์˜ค์ž„ - Mono.empty() - } - is IllegalArgumentException -> { - // 'flatMap'์—์„œ ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๋ฐœ์ƒ์‹œํ‚จ "์ด๋ฏธ ๋“ฑ๋ก๋œ ํšŒ์›" ์˜ค๋ฅ˜ - Mono.error(error) - } - else -> { - // ๊ทธ ์™ธ์˜ ๋ชจ๋“  ์„œ๋ฒ„ ๋‚ด๋ถ€ ํฌ๋ž˜์‹œ (NPE, DB ์˜ค๋ฅ˜ ๋“ฑ) - logService.log("!!! submitRank: checkAuthUsers ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ํฌ๋ž˜์‹œ ๋ฐœ์ƒ !!!", error) - Mono.error(IllegalArgumentException("์ด๋ฆ„ ํ™•์ธ ์ค‘ ์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.")) - } - } + // ๊ทธ ์™ธ 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 { rankWithSameName -> + // ๋žญํ‚น์ด ์กด์žฌํ•˜๋ฉด + if (rankWithSameName.userId == anonymousUserId) { + // ๊ทธ๊ฒŒ ๋‚ด ID์ž„ (์˜ˆ: "Bum"์œผ๋กœ ๋“ฑ๋ก ํ›„, "Bum"์œผ๋กœ ๋‹ค์‹œ ๋“ฑ๋ก) + // -> ํ†ต๊ณผ + Mono.empty() } else { - Mono.empty() + // ๋‚ด ID๊ฐ€ ์•„๋‹˜ (๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด "Bum" ์‚ฌ์šฉ ์ค‘) + // -> ์ค‘๋ณต ์˜ค๋ฅ˜ + Mono.error(IllegalArgumentException("์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.")) } } - .switchIfEmpty(Mono.defer { - rankRepository.findByPlayerName(requestedName).hasElements() - .flatMap { nameExists -> - if (nameExists) { - Mono.error(IllegalArgumentException("์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.")) - } else { - Mono.empty() - } - } - }) - // 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. ๋žญํ‚น ์ €์žฅ - return gameRankMono.flatMap { rankRepository.save(it) } + // 4. ๋žญํ‚น ์ €์žฅ + // [์ˆ˜์ •] 'findFirstByUserId'๋Š” ์ด์ œ ํ•„์š” ์—†์œผ๋ฏ€๋กœ 'flatMap' ๋Œ€์‹  'then' ์‚ฌ์šฉ + return gameRankMono.flatMap { rankRepository.save(it) } + } } /**