feat(user-creator-chat): WebSocket Redis presence를 추가한다

This commit is contained in:
2026-06-18 19:07:54 +09:00
parent afa57b70de
commit 216850c07a
3 changed files with 248 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.stereotype.Service
import java.time.Duration
import java.time.Instant
@Service
class UserCreatorChatPresenceService(
private val stringRedisTemplate: StringRedisTemplate,
private val objectMapper: ObjectMapper,
@Qualifier("userCreatorChatWebSocketServerId") private val serverId: String
) {
fun markJoined(roomId: Long, memberId: Long, sessionId: String) {
stringRedisTemplate.opsForValue().set(
presenceKey(roomId, memberId, sessionId),
presenceJson(roomId, memberId, sessionId),
PRESENCE_TTL
)
stringRedisTemplate.opsForSet().add(memberSessionsKey(roomId, memberId), sessionId)
stringRedisTemplate.expire(memberSessionsKey(roomId, memberId), PRESENCE_TTL)
stringRedisTemplate.opsForSet().add(roomKey(roomId), memberId.toString())
stringRedisTemplate.expire(roomKey(roomId), PRESENCE_TTL)
}
fun refresh(roomId: Long, memberId: Long, sessionId: String) {
stringRedisTemplate.opsForValue().set(
presenceKey(roomId, memberId, sessionId),
presenceJson(roomId, memberId, sessionId),
PRESENCE_TTL
)
stringRedisTemplate.expire(memberSessionsKey(roomId, memberId), PRESENCE_TTL)
stringRedisTemplate.expire(roomKey(roomId), PRESENCE_TTL)
}
fun markLeft(roomId: Long, memberId: Long, sessionId: String) {
stringRedisTemplate.delete(presenceKey(roomId, memberId, sessionId))
stringRedisTemplate.opsForSet().remove(memberSessionsKey(roomId, memberId), sessionId)
removeMemberPresenceIfNoLiveSession(roomId, memberId)
}
fun hasPresence(roomId: Long, memberId: Long): Boolean {
return findLiveSessionIds(roomId, memberId).isNotEmpty()
}
private fun removeMemberPresenceIfNoLiveSession(roomId: Long, memberId: Long) {
if (findLiveSessionIds(roomId, memberId).isNotEmpty()) {
return
}
stringRedisTemplate.delete(memberSessionsKey(roomId, memberId))
stringRedisTemplate.opsForSet().remove(roomKey(roomId), memberId.toString())
}
private fun findLiveSessionIds(roomId: Long, memberId: Long): Set<String> {
val sessionIds = stringRedisTemplate.opsForSet().members(memberSessionsKey(roomId, memberId)) ?: emptySet()
return sessionIds.filterTo(mutableSetOf()) { sessionId ->
val isLive = stringRedisTemplate.hasKey(presenceKey(roomId, memberId, sessionId)) == true
if (!isLive) {
stringRedisTemplate.opsForSet().remove(memberSessionsKey(roomId, memberId), sessionId)
}
isLive
}
}
private fun presenceJson(roomId: Long, memberId: Long, sessionId: String): String {
return objectMapper.writeValueAsString(
UserCreatorChatRedisPresence(
serverId = serverId,
memberId = memberId,
roomId = roomId,
sessionId = sessionId,
lastSeenAt = Instant.now()
)
)
}
companion object {
private val PRESENCE_TTL = Duration.ofSeconds(90)
fun presenceKey(roomId: Long, memberId: Long, sessionId: String): String {
return "v2:user-creator-chat:ws:presence:$roomId:$memberId:$sessionId"
}
fun memberSessionsKey(roomId: Long, memberId: Long): String {
return "v2:user-creator-chat:ws:room:$roomId:member:$memberId:sessions"
}
fun roomKey(roomId: Long): String {
return "v2:user-creator-chat:ws:room:$roomId"
}
}
}
data class UserCreatorChatRedisPresence(
val serverId: String,
val memberId: Long,
val roomId: Long,
val sessionId: String,
val lastSeenAt: Instant
)

View File

@@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.UUID
@Configuration
class UserCreatorChatWebSocketServerIdConfig {
@Bean
fun userCreatorChatWebSocketServerId(
@Value("\${user-creator-chat.websocket.server-id:}") configuredServerId: String
): String {
return configuredServerId.takeIf { it.isNotBlank() } ?: UUID.randomUUID().toString()
}
}