From d74bd12f83b188c42b7472c5f3b96b972dac3e92 Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Mon, 15 Sep 2025 20:00:03 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=94=BC=EB=B2=84=ED=83=80=EC=9E=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=EC=9D=BC=EB=B0=98=20=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A6=AD=EC=9D=B4=20=EA=B2=80=EC=A6=9D=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=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 | 3 +- .../snackgame/biz/domain/SnackgameBizV2.kt | 3 +- .../game/snackgame/core/domain/Snackgame.kt | 12 ++- .../snackgame/core/domain/item/FeverTime.kt | 58 ++++++++++---- .../core/domain/item/FeverTimeListener.kt | 6 +- .../core/service/dto/StreaksRequest.kt | 16 +--- .../snackgame/core/service/FeverTimeTest.kt | 78 +++++++++++++++++++ 7 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.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 89ac2fa..8b1e90e 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 @@ -67,8 +67,7 @@ open class SnackgameBiz( } private fun increaseScore(earn: Int) { - val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER - this.score += earn * multiplier + this.score += earn } override val metadata = SNACK_GAME_BIZ 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 146368a..9b2771d 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 @@ -61,8 +61,7 @@ open class SnackgameBizV2( } private fun increaseScore(earn: Int) { - val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER - this.score += earn * multiplier + this.score += earn } override val metadata = SNACK_GAME_BIZ_V2 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 612ab3c..d0581e1 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 @@ -64,12 +64,16 @@ open class Snackgame( } private fun isFever(streakWithFever: StreakWithFever): Int { - val serverIsFever = feverTime?.isActive(streakWithFever.occurredAt) == true - val isValid = streakWithFever.clientIsFever && serverIsFever - val multiplier = if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER - return multiplier + val serverFever = feverTime + if (serverFever == null) return NORMAL_MULTIPLIER + val serverIsActive = serverFever.isActive(streakWithFever.occurredAt) + val feverStreakValidate = serverFever.validateFeverStreakOccurredAt(streakWithFever.occurredAt) + val isValid = streakWithFever.clientIsFever && serverIsActive && feverStreakValidate + + return if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER } + private fun increaseScore(earn: Int) { this.score += earn } 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 fabd527..486c106 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 @@ -1,5 +1,6 @@ package com.snackgame.server.game.snackgame.core.domain.item +import com.snackgame.server.game.snackgame.exception.InvalidStreakTimeException import java.time.Duration import java.time.LocalDateTime import javax.persistence.Embeddable @@ -7,34 +8,59 @@ import javax.persistence.Embeddable @Embeddable class FeverTime( private var feverStartedAt: LocalDateTime? = null, - private var feverPausedAt: LocalDateTime? = null + private var feverRemains: Duration, + private var lastResumedAt: LocalDateTime? = null, + private var paused: Boolean? = false, ) { - fun isActive(now: LocalDateTime = LocalDateTime.now()): Boolean { - if (feverStartedAt == null) return false - val effectiveStart = feverPausedAt?.let { feverStartedAt!!.plus(Duration.between(it, now)) } ?: feverStartedAt - return Duration.between(effectiveStart, now) < FEVER_TIME_PERIOD + + fun isActive(at: LocalDateTime): Boolean { + if (paused!! || feverRemains <= Duration.ZERO) return false + + val feverUsed = Duration.between(lastResumedAt, at) + return (feverRemains - feverUsed) > Duration.ZERO } - fun pause() { - if (feverStartedAt != null && feverPausedAt == null) { - feverPausedAt = LocalDateTime.now() + fun pause(at: LocalDateTime) { + if (!paused!! && lastResumedAt != null) { + val feverUsed = Duration.between(lastResumedAt, at) + feverRemains = (feverRemains - feverUsed).coerceAtLeast(Duration.ZERO) + paused = true + lastResumedAt = null } } - fun resume() { - if (feverStartedAt != null && feverPausedAt != null) { - val now = LocalDateTime.now() - val pausedDuration = Duration.between(feverPausedAt, now) - feverStartedAt = feverStartedAt!!.plus(pausedDuration) - feverPausedAt = null + fun resume(at: LocalDateTime) { + if (paused!! && feverRemains > Duration.ZERO) { + paused = false + lastResumedAt = at } } + fun validateFeverStreakOccurredAt(streakOccurredAt: LocalDateTime): Boolean { + val feverEndAt = calculateFeverEnd(streakOccurredAt) + if (streakOccurredAt.isBefore(feverStartedAt) || streakOccurredAt.isAfter(feverEndAt)) + throw InvalidStreakTimeException() + return true + } + + private fun calculateFeverEnd(at: LocalDateTime): LocalDateTime { + val progressed = (!paused!! && lastResumedAt != null) + .takeIf { it }?.let { Duration.between(lastResumedAt, at) } ?: Duration.ZERO + + val remaining = (feverRemains - progressed).coerceAtLeast(Duration.ZERO) + return at.minus(progressed).plus(remaining) + } + companion object { private val FEVER_TIME_PERIOD: Duration = Duration.ofSeconds(30) fun start(now: LocalDateTime = LocalDateTime.now()): FeverTime { - return FeverTime(now) + return FeverTime( + feverStartedAt = now, + feverRemains = FEVER_TIME_PERIOD, + lastResumedAt = now, + paused = false + ) } } -} \ No newline at end of file +} diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt index 76bcab4..5e8f95c 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt @@ -15,12 +15,12 @@ class FeverTimeListener( @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) fun onSessionPaused(event: SessionPauseEvent) { val game = snackgameRepository.getBy(event.ownerId, event.sessionId) - game.feverTime?.pause() + game.feverTime?.pause(event.occurredAt) } @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) fun onSessionResumed(event: SessionResumeEvent) { val game = snackgameRepository.getBy(event.ownerId, event.sessionId) - game.feverTime?.resume() + game.feverTime?.resume(event.occurredAt) } -} \ No newline at end of file +} diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt b/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt index 26eed98..45fb210 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt @@ -3,15 +3,13 @@ package com.snackgame.server.game.snackgame.core.service.dto import com.fasterxml.jackson.annotation.JsonCreator import com.snackgame.server.game.snackgame.core.domain.Coordinate import com.snackgame.server.game.snackgame.core.domain.Streak -import com.snackgame.server.game.snackgame.exception.InvalidStreakTimeException -import java.time.Duration import java.time.LocalDateTime data class StreaksRequest @JsonCreator constructor( val streaks: List ) { - fun toStreaks(now: LocalDateTime = LocalDateTime.now()): List = - streaks.map { it.toDomain(now) } + fun toStreaks(): List = + streaks.map { it.toDomain() } } data class StreakWithMeta( @@ -19,21 +17,13 @@ data class StreakWithMeta( val isFever: Boolean, val occurredAt: LocalDateTime, ) { - fun toDomain(now: LocalDateTime): StreakWithFever { - if (Duration.between(occurredAt, now).abs() > REQUEST_TIME_TOLERANCE) { - throw InvalidStreakTimeException() - } - + fun toDomain(): StreakWithFever { return StreakWithFever( streak = Streak.of(coordinates.map { Coordinate(it.y, it.x) }), clientIsFever = isFever, occurredAt = occurredAt ) } - - companion object { - private val REQUEST_TIME_TOLERANCE: Duration = Duration.ofSeconds(2) - } } data class StreakWithFever( 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 new file mode 100644 index 0000000..06901f4 --- /dev/null +++ b/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt @@ -0,0 +1,78 @@ +@file:Suppress("NonAsciiCharacters") + +package com.snackgame.server.game.snackgame.core.service + +import com.snackgame.server.game.snackgame.core.domain.item.FeverTime +import com.snackgame.server.game.snackgame.exception.InvalidStreakTimeException +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDateTime + +class FeverTimeTest { + + @Test + fun `일시정지가 되면 피버타임이 비활성화 된다`() { + val startTime = LocalDateTime.now() + val pauseTime = startTime.plusSeconds(10) + val fever = FeverTime.start(startTime) + + fever.pause(pauseTime) + + assertFalse(fever.isActive(pauseTime)) + } + + @Test + fun `피버타임을 재개하면 다시 시간을 재야한다`() { + val startTime = LocalDateTime.now() + val pauseTime = startTime.plusSeconds(10) + val resumeTime = pauseTime.plusSeconds(5) + val fever = FeverTime.start(startTime) + + fever.pause(pauseTime) + fever.resume(resumeTime) + + assertTrue(fever.isActive(resumeTime)) + } + + @Test + fun `isActive should return false after fever ends`() { + val startTime = LocalDateTime.of(2025, 9, 8, 8, 0, 0) + val endTime = startTime.plusSeconds(31) + val fever = FeverTime.start(startTime) + + assertFalse(fever.isActive(endTime)) + } + + @Test + fun `validateFeverStreakOccurredAt should correctly validate client occurrence`() { + val startTime = LocalDateTime.of(2025, 9, 8, 8, 0, 0) + val fever = FeverTime.start(startTime) + + val validTime = startTime.plusSeconds(10) + val invalidTime = startTime.plusSeconds(40) + + assertTrue(fever.validateFeverStreakOccurredAt(validTime)) + assertThrows(InvalidStreakTimeException::class.java) { fever.validateFeverStreakOccurredAt(invalidTime) } + } + + @Test + fun `pause and resume multiple times should correctly update remaining time`() { + val startTime = LocalDateTime.of(2025, 9, 8, 8, 0, 0) + val fever = FeverTime.start(startTime) + + val pause1 = startTime.plusSeconds(10) + val resume1 = pause1.plusSeconds(5) + val pause2 = resume1.plusSeconds(5) + val resume2 = pause2.plusSeconds(3) + + fever.pause(pause1) + fever.resume(resume1) + fever.pause(pause2) + fever.resume(resume2) + + val checkTime = resume2.plusSeconds(10) + assertTrue(fever.isActive(checkTime)) // 남은 시간 고려 + } +}