Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(snackgame.biz): 기존 인증체계를 사용하지 않는다 #189

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ enum class Metadata(
SNACK_GAME(2, "스낵게임"),
SNACK_GAME_INFINITE(3, "스낵게임 무한모드"),
SNACK_GAME_BIZ(4, "스낵게임 Biz"),
SNACK_GAME_BIZ_V2(5, "스낵게임 Biz"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.snackgame.server.game.snackgame.biz.controller

import com.snackgame.server.auth.token.util.JwtProvider
import com.snackgame.server.game.sign.service.Signed
import com.snackgame.server.game.snackgame.biz.service.SnackgameBizV2Service
import com.snackgame.server.game.snackgame.biz.service.dto.SnackgameBizStartResponse
import com.snackgame.server.game.snackgame.core.service.dto.SnackgameEndResponse
import com.snackgame.server.game.snackgame.core.service.dto.SnackgameResponse
import com.snackgame.server.game.snackgame.core.service.dto.StreaksRequest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.net.InetAddress
import kotlin.random.Random

@Tag(name = "🍿 스낵게임 Biz V2")
@RequestMapping("/games/5")
@RestController
class SnackgameBizV2Controller(
private val snackgameBizService: SnackgameBizV2Service,
private val accessTokenProvider: JwtProvider
) {

@Operation(
summary = "스낵게임 세션 시작",
description = """
스낵게임 세션을 시작한다."""
)
@PostMapping
fun startSessionFor(
@RequestHeader("X-Forwarded-For", required = false) originalIp: String?
): SnackgameBizStartResponse {
// TODO: 사용자를 직접 식별하여 id를 만든다.
val temporaryUserId = originalIp?.let { ipToLong(it) } ?: Random.nextLong()

val game = snackgameBizService.startSessionFor(temporaryUserId)

val token = accessTokenProvider.createTokenWith(temporaryUserId.toString())

return SnackgameBizStartResponse.of(game, token)
}

// TODO: 사용자를 직접 식별하여 id를 만든다.
fun ipToLong(ip: String): Long {
val inetAddress = InetAddress.getByName(ip)
val addressBytes = inetAddress.address
var result: Long = 0
for (byte in addressBytes) {
result = (result shl 8) or (byte.toInt() and 0xFF).toLong()
}
return result
}

@Operation(
summary = "스트릭 추가",
description = """
스트릭을 순서대로 전달하여 게임을 검증한다.
황금 스낵을 제거한 경우 세션 정보가 함께 응답된다.
"""
)
@PostMapping("/{sessionId}/streaks")
fun removeStreaks(
@RequestHeader("Authorization") authorization: String,
@PathVariable sessionId: Long,
@RequestBody streaksRequest: StreaksRequest
): ResponseEntity<SnackgameResponse> {
val memberId = accessTokenProvider.getSubjectFrom(authorization).toLong()
val game = snackgameBizService.removeStreaks(memberId, sessionId, streaksRequest)

return ResponseEntity
.status(HttpStatus.OK)
.body(game)
}

@Operation(
summary = "스낵게임 세션 일시정지",
description = """
해당 세션이 일시정지된다.

일시정지된 세션은 별도로 종료하지 않아도 **7일** 후 자동으로 만료된다."""
)
@PostMapping("/{sessionId}/pause")
fun pause(
@RequestHeader("Authorization") authorization: String,
@PathVariable sessionId: Long
): SnackgameResponse {
val memberId = accessTokenProvider.getSubjectFrom(authorization).toLong()
return snackgameBizService.pause(memberId, sessionId)
}

@Operation(summary = "스낵게임 세션 재개", description = "해당 세션을 재개한다")
@PostMapping("/{sessionId}/resume")
fun resume(
@RequestHeader("Authorization") authorization: String,
@PathVariable sessionId: Long
): SnackgameResponse {
val memberId = accessTokenProvider.getSubjectFrom(authorization).toLong()
return snackgameBizService.resume(memberId, sessionId)
}

@Signed
@Operation(summary = "스낵게임 세션 종료", description = "세션을 종료한다")
@PostMapping("/{sessionId}/end")
fun end(
@RequestHeader("Authorization") authorization: String,
@PathVariable sessionId: Long
): SnackgameEndResponse {
val memberId = accessTokenProvider.getSubjectFrom(authorization).toLong()
return snackgameBizService.end(memberId, sessionId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.snackgame.server.game.snackgame.biz.domain

import com.snackgame.server.game.metadata.Metadata.SNACK_GAME_BIZ_V2
import com.snackgame.server.game.session.domain.Session
import com.snackgame.server.game.snackgame.core.domain.Board
import com.snackgame.server.game.snackgame.core.domain.BoardConverter
import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_HEIGHT
import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_WIDTH
import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SESSION_TIME
import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SPARE_TIME
import com.snackgame.server.game.snackgame.core.domain.Streak
import com.snackgame.server.game.snackgame.core.domain.snack.Snack
import java.time.Duration
import javax.persistence.Convert
import javax.persistence.Entity
import javax.persistence.Lob

@Entity
open class SnackgameBizV2(
ownerId: Long,
board: Board = Board(DEFAULT_HEIGHT, DEFAULT_WIDTH),
timeLimit: Duration = SESSION_TIME + SPARE_TIME,
score: Int = 0
) : Session(ownerId, timeLimit, score) {

@Lob
@Convert(converter = BoardConverter::class)
var board = board
private set

fun remove(streak: Streak) {
val removedSnacks = board.removeSnacksIn(streak)
this.score += removedSnacks.size
if (removedSnacks.any(Snack::isGolden)) {
this.board = board.reset()
}
}

override val metadata = SNACK_GAME_BIZ_V2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.snackgame.server.game.snackgame.biz.domain

import com.snackgame.server.game.session.exception.NoSuchSessionException
import com.snackgame.server.game.snackgame.core.domain.Percentile
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface SnackgameBizV2Repository : JpaRepository<SnackgameBizV2, Long> {
fun findByOwnerIdAndSessionId(ownerId: Long, sessionId: Long): SnackgameBizV2?

@Query(
value = """
with scores as (
select percent_rank() over (order by score desc) as percentile, session_id, score
from snackgame_biz_v2 where TIMESTAMPDIFF(SECOND, now(), expires_at) <=0
)
select percentile from scores where session_id = :sessionId""",
nativeQuery = true
)
fun findPercentileOf(sessionId: Long): Double?
}

fun SnackgameBizV2Repository.getBy(ownerId: Long, sessionId: Long): SnackgameBizV2 =
findByOwnerIdAndSessionId(ownerId, sessionId) ?: throw NoSuchSessionException()

fun SnackgameBizV2Repository.ratePercentileOf(sessionId: Long): Percentile {
with(findPercentileOf(sessionId)) {
this ?: throw NoSuchSessionException()
return Percentile(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.snackgame.server.game.snackgame.biz.service


import com.snackgame.server.game.session.event.SessionEndEvent
import com.snackgame.server.game.snackgame.biz.domain.SnackgameBizV2
import com.snackgame.server.game.snackgame.biz.domain.SnackgameBizV2Repository
import com.snackgame.server.game.snackgame.biz.domain.getBy
import com.snackgame.server.game.snackgame.biz.domain.ratePercentileOf
import com.snackgame.server.game.snackgame.core.service.dto.SnackgameEndResponse
import com.snackgame.server.game.snackgame.core.service.dto.SnackgameResponse
import com.snackgame.server.game.snackgame.core.service.dto.StreaksRequest
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class SnackgameBizV2Service(
private val snackgameBizRepository: SnackgameBizV2Repository,
private val eventPublisher: ApplicationEventPublisher,
) {

@Transactional
fun startSessionFor(memberId: Long): SnackgameResponse {
val game = snackgameBizRepository.save(SnackgameBizV2(memberId))

return SnackgameResponse.of(game)
}

@Transactional
fun removeStreaks(memberId: Long, sessionId: Long, streaksRequest: StreaksRequest): SnackgameResponse {
val game = snackgameBizRepository.getBy(memberId, sessionId)

streaksRequest.toStreaks()
.forEach { game.remove(it) }

return SnackgameResponse.of(game)
}

@Transactional
fun pause(memberId: Long, sessionId: Long): SnackgameResponse {
val game = snackgameBizRepository.getBy(memberId, sessionId)

game.pause()

return SnackgameResponse.of(game)
}

@Transactional
fun resume(memberId: Long, sessionId: Long): SnackgameResponse {
val game = snackgameBizRepository.getBy(memberId, sessionId)

game.resume()

return SnackgameResponse.of(game)
}

@Transactional
fun end(memberId: Long, sessionId: Long): SnackgameEndResponse {
val game = snackgameBizRepository.getBy(memberId, sessionId)

game.end()
eventPublisher.publishEvent(SessionEndEvent.of(game))

return SnackgameEndResponse.of(game, snackgameBizRepository.ratePercentileOf(sessionId))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.snackgame.server.game.snackgame.biz.service.dto

import com.snackgame.server.game.metadata.MetadataResponse
import com.snackgame.server.game.session.domain.SessionStateType
import com.snackgame.server.game.snackgame.core.service.dto.SnackResponse
import com.snackgame.server.game.snackgame.core.service.dto.SnackgameResponse
import java.time.LocalDateTime

data class SnackgameBizStartResponse(
val metadata: MetadataResponse,
val ownerId: Long,
val sessionId: Long,
val state: SessionStateType,
val score: Int,
val createdAt: LocalDateTime,
val board: List<List<SnackResponse>>,
val token: String
) {

companion object {

fun of(snackgame: SnackgameResponse, token: String): SnackgameBizStartResponse {
return SnackgameBizStartResponse(
snackgame.metadata,
snackgame.ownerId,
snackgame.sessionId,
snackgame.state,
snackgame.score,
snackgame.createdAt,
snackgame.board,
token
)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package com.snackgame.server.game.snackgame.core.service.dto
import com.snackgame.server.game.session.domain.Session
import com.snackgame.server.game.session.service.dto.SessionResponse
import com.snackgame.server.game.snackgame.biz.domain.SnackgameBiz
import com.snackgame.server.game.snackgame.biz.domain.SnackgameBizV2
import com.snackgame.server.game.snackgame.core.domain.Percentile
import com.snackgame.server.game.snackgame.core.domain.Snackgame
import com.snackgame.server.game.snackgame.infinite.domain.SnackgameInfinite
Expand Down Expand Up @@ -36,5 +37,12 @@ class SnackgameEndResponse(
percentile.percentage()
)
}

fun of(snackgame: SnackgameBizV2, percentile: Percentile): SnackgameEndResponse {
return SnackgameEndResponse(
snackgame,
percentile.percentage()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.snackgame.server.game.snackgame.core.service.dto
import com.snackgame.server.game.session.domain.Session
import com.snackgame.server.game.session.service.dto.SessionResponse
import com.snackgame.server.game.snackgame.biz.domain.SnackgameBiz
import com.snackgame.server.game.snackgame.biz.domain.SnackgameBizV2
import com.snackgame.server.game.snackgame.core.domain.Snackgame
import com.snackgame.server.game.snackgame.infinite.domain.SnackgameInfinite

Expand All @@ -20,18 +21,25 @@ class SnackgameResponse(
)
}

fun of(snackgame: SnackgameInfinite): SnackgameResponse {
fun of(snackgame: SnackgameBiz): SnackgameResponse {
return SnackgameResponse(
snackgame,
listOf() // TODO: bring Board in
SnackResponse.of(snackgame.board.getSnacks())
)
}

fun of(snackgame: SnackgameBiz): SnackgameResponse {
fun of(snackgame: SnackgameBizV2): SnackgameResponse {
return SnackgameResponse(
snackgame,
SnackResponse.of(snackgame.board.getSnacks())
)
}

fun of(snackgame: SnackgameInfinite): SnackgameResponse {
return SnackgameResponse(
snackgame,
listOf() // TODO: bring Board in
)
}
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/snackgame/server/status/StatusService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class StatusService(private val memberRepository: MemberRepository) {
Metadata.APPLE_GAME,
Metadata.SNACK_GAME_INFINITE,
Metadata.SNACK_GAME_BIZ -> addExpWithScore(session)

else -> {}
}
}

Expand Down
Loading
Loading