From 05ec13e9e1fe3372cb081ef13c320527e601510f Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Wed, 21 Jan 2026 16:06:46 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EC=B9=98=ED=8C=85=20=EB=B0=8F=20?= =?UTF-8?q?=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20=EC=A7=80=EC=97=B0=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/snackgame/biz/domain/SnackgameBiz.kt | 6 ++++- .../snackgame/biz/domain/SnackgameBizV2.kt | 6 ++++- .../game/snackgame/core/domain/Snackgame.kt | 6 ++++- .../snackgame/core/domain/item/FeverTime.kt | 27 ++++++++++++++++--- .../exception/InvalidStreakTimeException.kt | 4 --- .../snackgame/core/domain/SnackgameTest.kt | 14 +++++++--- .../snackgame/core/service/FeverTimeTest.kt | 16 +++++------ 7 files changed, 56 insertions(+), 23 deletions(-) delete mode 100644 src/main/java/com/snackgame/server/game/snackgame/exception/InvalidStreakTimeException.kt diff --git a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt index 1df62f4..8a51149 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt @@ -66,7 +66,11 @@ open class SnackgameBiz( private fun calculateMultiplier(streakWithFever: StreakWithFever): Int { val serverFever = feverTime ?: return NORMAL_MULTIPLIER - if (streakWithFever.clientIsFever && serverFever.isFeverTime(streakWithFever.occurredAt)) { + if (streakWithFever.clientIsFever && + serverFever.isFeverTime(streakWithFever.occurredAt) && + serverFever.canApplyFeverMultiplier() + ) { + serverFever.incrementFeverStreak() return FEVER_MULTIPLIER } return NORMAL_MULTIPLIER diff --git a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt index 8d942f1..775ad63 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt @@ -60,7 +60,11 @@ open class SnackgameBizV2( private fun calculateMultiplier(streakWithFever: StreakWithFever): Int { val serverFever = feverTime ?: return NORMAL_MULTIPLIER - if (streakWithFever.clientIsFever && serverFever.isFeverTime(streakWithFever.occurredAt)) { + if (streakWithFever.clientIsFever && + serverFever.isFeverTime(streakWithFever.occurredAt) && + serverFever.canApplyFeverMultiplier() + ) { + serverFever.incrementFeverStreak() return FEVER_MULTIPLIER } return NORMAL_MULTIPLIER diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt index b44965b..de510b9 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt @@ -67,7 +67,11 @@ open class Snackgame( private fun calculateMultiplier(streakWithFever: StreakWithFever): Int { val serverFever = feverTime ?: return NORMAL_MULTIPLIER - if (streakWithFever.clientIsFever && serverFever.isFeverTime(streakWithFever.occurredAt)) { + if (streakWithFever.clientIsFever && + serverFever.isFeverTime(streakWithFever.occurredAt) && + serverFever.canApplyFeverMultiplier() + ) { + serverFever.incrementFeverStreak() return FEVER_MULTIPLIER } return NORMAL_MULTIPLIER diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt index 007a89c..129dd7b 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt @@ -9,18 +9,34 @@ class FeverTime( val feverStartedAt: LocalDateTime, var feverEndAt: LocalDateTime, var lastPausedAt: LocalDateTime? = null, - var paused: Boolean? = false + var paused: Boolean? = false, + var feverStreakCount: Int? = 0 ) { - fun isFeverTime(occurredAt: LocalDateTime): Boolean { + fun isFeverTime(occurredAt: LocalDateTime, serverNow: LocalDateTime = LocalDateTime.now()): Boolean { + // 1. 서버 시간 기준 합리성 검증 (치팅 방지) + if (occurredAt.isBefore(serverNow.minus(MAX_PAST_ALLOWED)) || + occurredAt.isAfter(serverNow.plus(MAX_FUTURE_ALLOWED)) + ) { + return false + } + + // 2. 피버타임 범위 검증 (서버 연산 및 네트워크 지연 고려) + val validEndAt = feverEndAt.plus(BUFFER_DURATION) val validStartedAt = feverStartedAt.minus(BUFFER_DURATION) - if (occurredAt.isBefore(validStartedAt) || occurredAt.isAfter(feverEndAt)) { + if (occurredAt.isBefore(validStartedAt) || occurredAt.isAfter(validEndAt)) { return false } return true } + fun canApplyFeverMultiplier(): Boolean = (feverStreakCount ?: 0) < MAX_FEVER_STREAKS + + fun incrementFeverStreak() { + feverStreakCount = (feverStreakCount ?: 0) + 1 + } + fun pause(at: LocalDateTime) { if (paused != true) { this.paused = true @@ -31,7 +47,7 @@ class FeverTime( fun resume(at: LocalDateTime) { if (paused == true && lastPausedAt != null) { val pausedDuration = Duration.between(lastPausedAt, at) - // 쉬었던 만큼 종료 시간을 뒤로 미룸 + this.feverEndAt = this.feverEndAt.plus(pausedDuration) this.paused = false @@ -42,6 +58,9 @@ class FeverTime( companion object { private val FEVER_DURATION: Duration = Duration.ofSeconds(30) private val BUFFER_DURATION: Duration = Duration.ofSeconds(1) + private val MAX_PAST_ALLOWED: Duration = Duration.ofMinutes(3) + private val MAX_FUTURE_ALLOWED: Duration = Duration.ofSeconds(5) + const val MAX_FEVER_STREAKS = 60 fun start(now: LocalDateTime = LocalDateTime.now()): FeverTime { return FeverTime( diff --git a/src/main/java/com/snackgame/server/game/snackgame/exception/InvalidStreakTimeException.kt b/src/main/java/com/snackgame/server/game/snackgame/exception/InvalidStreakTimeException.kt deleted file mode 100644 index df23b33..0000000 --- a/src/main/java/com/snackgame/server/game/snackgame/exception/InvalidStreakTimeException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.snackgame.server.game.snackgame.exception - -class InvalidStreakTimeException : SnackgameException("허용된 시간차를 초과하였습니다.") { -} \ No newline at end of file diff --git a/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt b/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt index 976fcea..37ce724 100644 --- a/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt +++ b/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt @@ -111,16 +111,22 @@ class SnackgameTest { @Test fun `일시정지_후_재개하면_피버타임도_연장되어_점수_2배가_적용된다`() { val game = Snackgame(땡칠().id, BoardFixture.TWO_BY_FOUR()) + val feverStartTime = LocalDateTime.now() game.startFeverTime() - game.feverTime!!.pause(LocalDateTime.now().plusSeconds(10)) + // 피버타임 10초 후 일시정지 + val pauseAt = feverStartTime.plusSeconds(10) + game.feverTime!!.pause(pauseAt) - game.feverTime!!.resume(LocalDateTime.now().plusHours(1)) + // 20초 일시정지 후 재개 (총 30초 경과, 하지만 피버는 10초만 흘렀음) + val resumeAt = pauseAt.plusSeconds(20) + game.feverTime!!.resume(resumeAt) val streak = Streak.of(arrayListOf(Coordinate(0, 0), Coordinate(1, 0))) - - val occurredAt = LocalDateTime.now().plusHours(1).plusSeconds(5) + // 재개 후 5초 뒤 (피버타임 15초 시점, 아직 30초 내) + // 서버 시간 기준으로는 현재 시간 + 약간의 오차로 설정 + val occurredAt = LocalDateTime.now().plusSeconds(2) val request = StreakWithFever(streak, clientIsFever = true, occurredAt = occurredAt) game.remove(request) diff --git a/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt b/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt index 5d7b713..e202c46 100644 --- a/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt +++ b/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt @@ -17,14 +17,14 @@ class FeverTimeTest { val end = start.plusSeconds(30) val feverTime = FeverTime(start, end) - - assertThat(feverTime.isFeverTime(start)).isTrue - assertThat(feverTime.isFeverTime(end)).isTrue - assertThat(feverTime.isFeverTime(start.plusSeconds(15))).isTrue - - - assertThat(feverTime.isFeverTime(start.minusSeconds(2))).isFalse - assertThat(feverTime.isFeverTime(end.plusSeconds(2))).isFalse + // 서버 시간 검증은 ±3분/5초 허용하므로, 각 occurredAt 근처로 serverNow 설정 + assertThat(feverTime.isFeverTime(start, start)).isTrue + assertThat(feverTime.isFeverTime(end, end)).isTrue + assertThat(feverTime.isFeverTime(start.plusSeconds(15), start.plusSeconds(15))).isTrue + + // 피버타임 범위 밖이면서, 서버 시간은 실제 요청 시점으로 설정 + assertThat(feverTime.isFeverTime(start.minusSeconds(2), start.minusSeconds(2))).isFalse + assertThat(feverTime.isFeverTime(end.plusSeconds(2), end.plusSeconds(2))).isFalse } @Test From 4074c486253446c6972aaf1ffd3fad06a584a911 Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Wed, 21 Jan 2026 16:13:27 +0900 Subject: [PATCH 2/3] =?UTF-8?q?comment:=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/codeStyles/Project.xml | 1 + .../snackgame/core/service/SnackgameService.kt | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 4953632..dbff939 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -5,6 +5,7 @@