Skip to content

Commit

Permalink
Merge pull request #90 from mash-up-kr/yaeoni/pick-notification
Browse files Browse the repository at this point in the history
feat; pick 을 생성하면 pick 대상에게 알림을 보내요. (SSE기반)
  • Loading branch information
yaeoni authored Aug 16, 2024
2 parents 58859d2 + 06cf355 commit aec1738
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 1 deletion.
7 changes: 7 additions & 0 deletions _endpoint_test/notification.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### SSE stream 연결
GET {{host}}/notification-stream
Authorization: {{authorization}}

### notification test
POST {{host}}/notification-test?questionId=testquestion&pickId=testetsqpickpick
Authorization: {{authorization}}
52 changes: 52 additions & 0 deletions api/src/main/kotlin/com/mashup/dojo/NotificationSseController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.mashup.dojo

import com.mashup.dojo.config.security.MemberPrincipalContextHolder
import com.mashup.dojo.domain.PickId
import com.mashup.dojo.domain.QuestionId
import com.mashup.dojo.service.MemberService
import com.mashup.dojo.service.SSENotificationService
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.oas.annotations.Operation
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter

@RestController
class NotificationSseController(
private val sseNotificationService: SSENotificationService,
private val memberService: MemberService,
) {
private val logger = KotlinLogging.logger { }

@GetMapping("/notification-stream")
@Operation(
summary = "알림을 받기 위해 SSE 커넥션을 연결합니다, timeout: 3분"
)
fun stream(): SseEmitter {
logger.info { "connect sse" }
// timeout(3분) 동안 아무런 데이터 전송되지 않으면 연결 자동 종료
val emitter = SseEmitter(180000L)
val memberId = MemberPrincipalContextHolder.current().id
sseNotificationService.addEmitter(memberId = memberId, emitter = emitter)
return emitter
}

@PostMapping("/notification-test")
@Operation(
summary = "알림 테스트를 위해 사용합니다.",
description = "해당 API 요청자에게 event가 발송됩니다."
)
fun test(
questionId: String,
pickId: String,
) {
val memberId = MemberPrincipalContextHolder.current().id
val member = memberService.findMemberById(memberId) ?: return
sseNotificationService.notifyPicked(
target = member,
questionId = QuestionId(questionId),
pickId = PickId(pickId)
)
}
}
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ project(":service") {
dependencies {
api(project(":entity"))
api(project(":common"))

// for sse
implementation("org.springframework.boot:spring-boot-starter-web")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.mashup.dojo.service

import com.mashup.dojo.domain.Member
import com.mashup.dojo.domain.MemberId
import com.mashup.dojo.domain.PickId
import com.mashup.dojo.domain.QuestionId
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
import java.util.concurrent.ConcurrentHashMap

interface NotificationService {
fun notifyPicked(
target: Member,
questionId: QuestionId,
pickId: PickId,
)
}

/**
* SSE (Server Sent Event) 방식으로 발송되는 알림
*/
@Service
class SSENotificationService : NotificationService {
private val emitters = ConcurrentHashMap<String, SseEmitter>()
private val logger = KotlinLogging.logger { }

override fun notifyPicked(
target: Member,
questionId: QuestionId,
pickId: PickId,
) {
data class NotifyPickedEvent(val questionId: QuestionId, val pickId: PickId, val ordinal: Int)

val emitter = emitters[target.id.value]
emitter?.send(
SseEmitter.event()
.name("picked")
.data(
NotifyPickedEvent(
questionId = questionId,
pickId = pickId,
ordinal = target.ordinal
)
)
) ?: run {
logger.warn { "emitter not found, memberId: ${target.id.value}" }
}
}

fun addEmitter(
memberId: MemberId,
emitter: SseEmitter,
) {
emitters[memberId.value] = emitter
emitter.onCompletion {
logger.debug { "emitter($memberId) completed successfully" }
emitters.remove(memberId.value)
}
emitter.onError {
logger.debug { "emitter error occurred: $it" }
emitters.remove(memberId.value)
}
emitter.onTimeout {
logger.debug { "emitter($memberId) timed out" }
emitters.remove(memberId.value)
}
}
}
10 changes: 9 additions & 1 deletion service/src/main/kotlin/com/mashup/dojo/usecase/PickUseCase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.mashup.dojo.domain.QuestionSetId
import com.mashup.dojo.domain.QuestionSheetId
import com.mashup.dojo.service.ImageService
import com.mashup.dojo.service.MemberService
import com.mashup.dojo.service.NotificationService
import com.mashup.dojo.service.PickService
import com.mashup.dojo.service.QuestionService
import com.mashup.dojo.usecase.PickUseCase.GetReceivedPick
Expand Down Expand Up @@ -93,6 +94,7 @@ class DefaultPickUseCase(
private val questionService: QuestionService,
private val imageService: ImageService,
private val memberService: MemberService,
private val notificationService: NotificationService,
) : PickUseCase {
override fun getReceivedPickList(command: GetReceivedPickListCommand): List<GetReceivedPick> {
val receivedPickList: List<Pick> = pickService.getReceivedPickList(command.memberId, command.sort)
Expand Down Expand Up @@ -147,7 +149,13 @@ class DefaultPickUseCase(
questionSheetId = command.questionSheetId,
pickerMemberId = command.pickerId,
pickedMemberId = pickedMember.id
)
).apply {
notificationService.notifyPicked(
pickId = this,
target = pickedMember,
questionId = question.id
)
}
}

override fun openPick(openPickCommand: OpenPickCommand): PickOpenInfo {
Expand Down

0 comments on commit aec1738

Please sign in to comment.