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 @@