Skip to content

Commit

Permalink
Merge pull request #75 from YAPP-Github/develop
Browse files Browse the repository at this point in the history
2024.08.25 운영 배포
  • Loading branch information
SunwoongH authored Aug 24, 2024
2 parents 526d8de + 73e2bc6 commit 8066da3
Show file tree
Hide file tree
Showing 68 changed files with 842 additions and 173 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
log/
/log
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.xorker.draw.mafia

import com.xorker.draw.exception.UnSupportedException
import com.xorker.draw.mafia.event.MafiaGameRandomMatchingEvent
import com.xorker.draw.websocket.WaitingQueueSession
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component

@Component
internal class MafiaGameWaitingQueueAdapter(
private val eventPublisher: ApplicationEventPublisher,
) : MafiaGameWaitingQueueRepository {
private val waitingQueue: ConcurrentHashMap<String, ConcurrentLinkedQueue<WaitingQueueSession>> = ConcurrentHashMap()

override fun enqueue(size: Int, session: WaitingQueueSession) {
val queue = waitingQueue.getOrPut(session.locale) { ConcurrentLinkedQueue() }

queue.add(session)

synchronized(this) {
if (queue.size >= size) {
val players = mutableListOf<WaitingQueueSession>()

(0 until size).forEach { _ ->
queue.poll().let {
players.add(it)
}
}

val event = MafiaGameRandomMatchingEvent(players)

eventPublisher.publishEvent(event)
}
}
}

override fun dequeue(session: WaitingQueueSession) {
val queue = waitingQueue[session.locale] ?: throw UnSupportedException

queue.remove(session)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.xorker.draw.mafia

import com.xorker.draw.room.RoomId
import com.xorker.draw.user.UserId

data class MafiaGameResult(
val roomId: RoomId,
val language: String,
val players: List<UserId>,
val mafia: UserId,
val category: String,
val answer: String,
val mafiaAnswer: String?,
val isMafiaWin: Boolean,
val drawData: MutableList<Pair<UserId, Map<String, Any>>>,
)

fun MafiaGameInfo.toMafiaGameResult(): MafiaGameResult {
val room = this.room

val phase = this.phase
assertIs<MafiaPhase.End>(phase)

return MafiaGameResult(
roomId = room.id,
language = room.locale,
players = room.players.map { it.userId },
mafia = phase.mafiaPlayer.userId,
category = phase.keyword.category,
answer = phase.keyword.answer,
mafiaAnswer = phase.answer,
isMafiaWin = phase.isMafiaWin,
drawData = phase.drawData,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.xorker.draw.mafia

import com.fasterxml.jackson.databind.ObjectMapper
import com.xorker.draw.exception.NotFoundWordException
import org.springframework.stereotype.Component

@Component
internal class MafiaGameResultAdapter(
private val objectMapper: ObjectMapper,
private val mafiaGameResultJpaRepository: MafiaGameResultJpaRepository,
private val wordJpaRepository: WordJpaRepository,
) : MafiaGameResultRepository {

override fun saveMafiaGameResult(gameInfo: MafiaGameInfo) {
val phase = gameInfo.phase
assertIs<MafiaPhase.End>(phase)

val keyword = phase.keyword
val findEntity = wordJpaRepository.findByKeyword(keyword.answer) ?: throw NotFoundWordException

val mafiaGameResult = gameInfo.toMafiaGameResult()

val serializedMafiaGameResult = objectMapper.writeValueAsString(mafiaGameResult)

val entity = MafiaGameResultJpaEntity.of(serializedMafiaGameResult, findEntity.id)

mafiaGameResultJpaRepository.save(entity)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.xorker.draw.mafia

import com.xorker.draw.BaseJpaEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name = "mafia_game_result")
internal class MafiaGameResultJpaEntity : BaseJpaEntity() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "mafia_game_result_id")
var id: Long = 0

@Column(name = "game_result", columnDefinition = "TEXT")
lateinit var gameResult: String
protected set

@Column(name = "word_id", columnDefinition = "bigint")
var wordId: Long = 0

companion object {
internal fun of(gameResult: String, wordId: Long): MafiaGameResultJpaEntity {
return MafiaGameResultJpaEntity().apply {
this.gameResult = gameResult
this.wordId = wordId
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.xorker.draw.mafia

import org.springframework.data.jpa.repository.JpaRepository

internal interface MafiaGameResultJpaRepository : JpaRepository<MafiaGameResultJpaEntity, Long>
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ internal interface WordJpaRepository : JpaRepository<WordJpaEntity, Long> {
"limit 1",
)
fun findRandomWord(@Param("locale") locale: String): WordJpaEntity

fun findByKeyword(keyword: String): WordJpaEntity?
}
3 changes: 3 additions & 0 deletions adapter/rdb/src/main/resources/application-rdb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ spring:
hibernate:
ddl-auto: validate

flyway:
baseline-on-migrate: true

---
spring.config.activate.on-profile: local, dev
spring:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
create table mafia_game_result
(
mafia_game_result_id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK',
game_result text NOT NULL COMMENT 'Serialized 마피아 게임 결과',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY PK_mafia_game_result (mafia_game_result_id),
KEY IDX_createdat(created_at),
KEY IDX_updatedat(updated_at)
) COMMENT '마피아 게임 결과';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alter table mafia_game_result
add column word_id bigint NOT NULL COMMENT 'Word 테이블 PK';
3 changes: 3 additions & 0 deletions app/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ dependencies {
implementation(project(":app:websocket"))
implementation(project(":core"))
implementation(project(":support:logging"))
implementation(project(":support:metric"))
implementation(project(":support:yaml"))

implementation("org.springframework.boot:spring-boot-starter-web:${Versions.SPRING_BOOT}")
implementation("org.springframework.boot:spring-boot-starter-validation:${Versions.SPRING_BOOT}")
implementation("org.springframework.boot:spring-boot-starter-aop:${Versions.SPRING_BOOT}")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${Versions.WEBMVC_UI}")
implementation("io.sentry:sentry-spring-boot-starter-jakarta:${Versions.SENTRY}")
implementation("io.sentry:sentry-logback:${Versions.SENTRY}")
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.xorker.draw.exception

import com.xorker.draw.support.logging.logger
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.http.HttpStatus
Expand All @@ -17,41 +18,54 @@ typealias ExceptionResponseEntity = ResponseEntity<ExceptionResponse>
class ApiExceptionHandler(
private val responseFactory: ExceptionResponseFactory,
) {
private val log = logger()

@ExceptionHandler(HttpRequestMethodNotSupportedException::class)
protected fun handleException(ex: HttpRequestMethodNotSupportedException): ExceptionResponseEntity {
log.warn(ex.message, ex)
return responseFactory.create(HttpStatus.METHOD_NOT_ALLOWED, InvalidRequestValueException)
}

@ExceptionHandler(MethodArgumentTypeMismatchException::class)
protected fun handleException(ex: MethodArgumentTypeMismatchException): ExceptionResponseEntity {
log.warn(ex.message, ex)
return responseFactory.create(InvalidRequestValueException)
}

@ExceptionHandler(MethodArgumentNotValidException::class)
protected fun handleException(ex: MethodArgumentNotValidException): ExceptionResponseEntity {
log.warn(ex.message, ex)
return responseFactory.create(InvalidRequestValueException)
}

@ExceptionHandler(BindException::class)
protected fun handleException(ex: BindException): ExceptionResponseEntity {
log.warn(ex.message, ex)
return responseFactory.create(InvalidRequestValueException)
}

@ExceptionHandler(XorkerException::class)
protected fun handleException(ex: XorkerException): ExceptionResponseEntity {
log.warn(ex.message, ex)
return responseFactory.create(ex)
}

@ExceptionHandler(ServerException::class)
protected fun handleException(ex: ServerException): ExceptionResponseEntity {
log.error(ex.message, ex)
return responseFactory.create(ex)
}

@ExceptionHandler(CriticalException::class)
protected fun handleException(ex: CriticalException): ExceptionResponseEntity {
// TODO: Alert Development , Discord Webhook or Sentry
log.error(ex.message, ex)
return responseFactory.create(ex)
}

@Order(value = Ordered.LOWEST_PRECEDENCE)
@ExceptionHandler(Exception::class)
protected fun handleException(ex: Exception): ResponseEntity<ExceptionResponse> {
ex.printStackTrace()
protected fun handleException(ex: Exception): ExceptionResponseEntity {
log.error(ex.message, ex)
return responseFactory.create(UnknownException(ex))
}
}
67 changes: 33 additions & 34 deletions app/api/src/main/kotlin/com/xorker/draw/log/ApiLoggingFilter.kt
Original file line number Diff line number Diff line change
@@ -1,63 +1,62 @@
package com.xorker.draw.log

import com.fasterxml.jackson.databind.ObjectMapper
import com.xorker.draw.support.logging.defaultApiJsonMap
import com.xorker.draw.support.logging.logger
import com.xorker.draw.support.logging.registerRequestId
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.annotation.WebFilter
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper

@Component
@WebFilter(urlPatterns = ["/api/*"])
@Order(Ordered.HIGHEST_PRECEDENCE)
class ApiLoggingFilter : Filter {
class ApiLoggingFilter(
private val objectMapper: ObjectMapper,
) : Filter {
private val logger = logger()

override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, chain: FilterChain) {
val request = RequestLoggingWrapper(servletRequest as HttpServletRequest)
val response = getResponseWrapper(servletResponse as HttpServletResponse)
registerRequestId()
val request = ContentCachingRequestWrapper(servletRequest as HttpServletRequest)
val response = ContentCachingResponseWrapper(servletResponse as HttpServletResponse)

try {
chain.doFilter(request, response)
} finally {
logger.info("${generateRequestLog(request)}\n${generateResponseLog(response)}")
logger.info(generateLog(request, response))
response.copyBodyToResponse()
MDC.clear()
}
}

private fun generateRequestLog(request: HttpServletRequest): String {
val log = StringBuilder("[Req]\n${request.method} ${request.requestURI}")

if (request.queryString.isNullOrBlank().not()) {
log.append("?${request.queryString}")
}

request.headerNames.iterator().forEach {
log.append("\n$it=${request.getHeader(it)}")
}

request.reader.lines().forEach {
log.append("\n$it")
}

return log.toString()
private fun generateLog(request: ContentCachingRequestWrapper, response: ContentCachingResponseWrapper): String {
val data = defaultApiJsonMap(
// Request 부분
"method" to request.method,
"uri" to request.requestURI,
"query" to request.queryString,
"header" to request.headerNames.asSequence()
.filterNot { it.lowercase() == "authorization" }
.map { it to request.getHeader(it) }
.toMap(),
"requestBody" to request.contentAsString,

// Response 부분
"status" to response.status,
"responseBody" to String(response.contentAsByteArray),
)

return objectMapper.writeValueAsString(data)
}

private fun generateResponseLog(response: ContentCachingResponseWrapper): String {
val log = StringBuilder("[Res]\n${response.status}")

val responseCacheWrapperObject = getResponseWrapper(response)

val responseStr = String(responseCacheWrapperObject.contentAsByteArray)

log.append("\n$responseStr")
return log.toString()
}

private fun getResponseWrapper(response: HttpServletResponse): ContentCachingResponseWrapper =
response as? ContentCachingResponseWrapper ?: ContentCachingResponseWrapper(response)
}
Loading

0 comments on commit 8066da3

Please sign in to comment.