-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #215 from bucket-back/OMCT-408-alarm-sse
[OMCT-408] 댓글 생성에 대한 알림 기능 구현
- Loading branch information
Showing
12 changed files
with
275 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
...java/com/programmers/bucketback/domains/comment/application/event/CommentCreateEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package com.programmers.bucketback.domains.comment.application.event; | ||
|
||
import java.util.Map; | ||
|
||
import com.programmers.bucketback.domains.comment.domain.Comment; | ||
import com.programmers.bucketback.domains.sse.PayLoadProvider; | ||
import com.programmers.bucketback.domains.sse.SsePayload; | ||
|
||
import lombok.Builder; | ||
|
||
@Builder | ||
public record CommentCreateEvent( | ||
Long commentWriterId, | ||
Long receiverId, | ||
Long feedId, | ||
String commentWriter | ||
) implements PayLoadProvider { | ||
|
||
public static SsePayload toSsePayload( | ||
String nickname, | ||
Comment comment | ||
) { | ||
CommentCreateEvent commentCreateEvent = of(nickname, comment); | ||
|
||
return SsePayload.builder() | ||
.receiverId(commentCreateEvent.receiverId()) | ||
.data(commentCreateEvent.getPayloadData()) | ||
.build(); | ||
} | ||
|
||
private static CommentCreateEvent of( | ||
final String nickname, | ||
final Comment comment | ||
) { | ||
CommentCreateEvent commentCreateEvent = CommentCreateEvent.builder() | ||
.commentWriterId(comment.getMemberId()) | ||
.receiverId(comment.getFeed().getMemberId()) | ||
.feedId(comment.getFeed().getId()) | ||
.commentWriter(nickname) | ||
.build(); | ||
return commentCreateEvent; | ||
} | ||
|
||
@Override | ||
public Map<String, Object> getPayloadData() { | ||
return Map.ofEntries( | ||
Map.entry("alarmMessage", CommentEventType.COMMENT_CREATE.getDescription()), | ||
Map.entry("memberId", this.commentWriterId()), | ||
Map.entry("receiverId", this.receiverId()), | ||
Map.entry("feedId", this.feedId()), | ||
Map.entry("commentWriter", this.commentWriter()) | ||
); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
...n/java/com/programmers/bucketback/domains/comment/application/event/CommentEventType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.programmers.bucketback.domains.comment.application.event; | ||
|
||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum CommentEventType { | ||
COMMENT_CREATE("댓글 생성"), | ||
COMMENT_ADOPTED("댓글 채택"); | ||
|
||
private final String description; | ||
} |
7 changes: 7 additions & 0 deletions
7
bucketback-api/src/main/java/com/programmers/bucketback/domains/sse/PayLoadProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.programmers.bucketback.domains.sse; | ||
|
||
import java.util.Map; | ||
|
||
public interface PayLoadProvider { | ||
Map<String, Object> getPayloadData(); | ||
} |
28 changes: 28 additions & 0 deletions
28
bucketback-api/src/main/java/com/programmers/bucketback/domains/sse/SseController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.programmers.bucketback.domains.sse; | ||
|
||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/sse") | ||
public class SseController { | ||
|
||
private final SseService sseService; | ||
|
||
@GetMapping(value = "/subscribe", produces = "text/event-stream;charset=utf-8") | ||
public ResponseEntity<SseEmitter> subscribe(HttpServletResponse response) { | ||
response.setHeader(HttpHeaders.CONNECTION, "keep-alive"); | ||
response.setHeader(HttpHeaders.CONTENT_TYPE, "text/event-stream;charset=utf-8"); | ||
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-transform"); | ||
|
||
return ResponseEntity.ok(sseService.subscribe()); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
bucketback-api/src/main/java/com/programmers/bucketback/domains/sse/SseEmitters.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package com.programmers.bucketback.domains.sse; | ||
|
||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
@Component | ||
public class SseEmitters { | ||
|
||
private static final Long DEFAULT_TIMEOUT = 30 * 60 * 1000L; // 60분 | ||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>(); | ||
|
||
public SseEmitter add(final Long receiverId) { | ||
SseEmitter sseEmitter = new SseEmitter(DEFAULT_TIMEOUT); | ||
|
||
this.emitters.put(receiverId, sseEmitter); | ||
sseEmitter.onTimeout(sseEmitter::complete); | ||
sseEmitter.onCompletion(() -> this.emitters.remove(receiverId)); | ||
sseEmitter.onError((e) -> this.emitters.remove(receiverId)); | ||
|
||
return sseEmitter; | ||
} | ||
|
||
public void send( | ||
final Long receiverId, | ||
final Object data | ||
) { | ||
SseEmitter sseEmitter = get(receiverId); | ||
try { | ||
sseEmitter.send(data); | ||
log.info("알람을 보냈습니다. userId : {}", receiverId); | ||
} catch (Exception e) { | ||
log.error("알람을 보내는 과정에서 오류가 발생했습니다."); | ||
throw new RuntimeException(); | ||
} | ||
} | ||
|
||
public SseEmitter get( | ||
final Long receiverId | ||
) { | ||
var sseEmitter = emitters.get(receiverId); | ||
if (sseEmitter == null) { | ||
log.warn("SSE를 구독하지 않은 유저입니다. userId : {}", receiverId); | ||
} | ||
return sseEmitter; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
bucketback-api/src/main/java/com/programmers/bucketback/domains/sse/SseEventListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.programmers.bucketback.domains.sse; | ||
|
||
import org.springframework.context.event.EventListener; | ||
import org.springframework.stereotype.Component; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class SseEventListener { | ||
|
||
private final SseEmitters sseEmitters; | ||
|
||
@EventListener | ||
public void sendSseEmitter(final SsePayload ssePayload) { | ||
sseEmitters.send( | ||
ssePayload.receiverId(), | ||
ssePayload.data() | ||
); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
bucketback-api/src/main/java/com/programmers/bucketback/domains/sse/SsePayload.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.programmers.bucketback.domains.sse; | ||
|
||
import java.util.Map; | ||
|
||
import lombok.Builder; | ||
|
||
@Builder | ||
public record SsePayload( | ||
String alarmType, | ||
Long receiverId, | ||
Map<String, Object> data | ||
) { | ||
} |
25 changes: 25 additions & 0 deletions
25
bucketback-api/src/main/java/com/programmers/bucketback/domains/sse/SseService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.programmers.bucketback.domains.sse; | ||
|
||
import org.springframework.stereotype.Service; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
||
import com.programmers.bucketback.global.config.security.SecurityUtils; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class SseService { | ||
private static final String DUMMY_DATA = "connected"; | ||
private final SseEmitters sseEmitters; | ||
|
||
public SseEmitter subscribe() { | ||
Long receiverId = SecurityUtils.getCurrentMemberId(); | ||
SseEmitter sseEmitter = sseEmitters.add(receiverId); | ||
|
||
//최초 연결 시점에는 더미 데이터 전송 | ||
sseEmitters.send(receiverId, DUMMY_DATA); | ||
|
||
return sseEmitter; | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
bucketback-api/src/test/java/com/programmers/bucketback/domains/sse/SseEmittersTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package com.programmers.bucketback.domains.sse; | ||
|
||
import static org.assertj.core.api.Assertions.*; | ||
|
||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
||
class SseEmittersTest { | ||
|
||
SseEmitters sseEmitters = new SseEmitters(); | ||
Long memberId = 1L; | ||
Long defaultTimeOut = 30 * 60 * 1000L; | ||
|
||
@Test | ||
@DisplayName("새로운 Emitters를 추가에 성공한다.") | ||
public void successAddEmitter() throws Exception { | ||
//when | ||
SseEmitter sseEmitter = sseEmitters.add(memberId); | ||
|
||
//then | ||
assertThat(sseEmitter.getTimeout()).isEqualTo(defaultTimeOut); | ||
} | ||
|
||
@Test | ||
@DisplayName("Emitters에 있는 정보를 가져올 수 있다.") | ||
public void successGetEmitter() throws Exception { | ||
//when | ||
sseEmitters.add(memberId); | ||
SseEmitter sseEmitter = sseEmitters.get(memberId); | ||
|
||
//then | ||
assertThat(sseEmitter).isNotNull(); | ||
} | ||
|
||
@Test | ||
@DisplayName("Emitters에 저장된 정보가 없다.") | ||
public void failGetEmitter() throws Exception { | ||
//when | ||
SseEmitter sseEmitter = sseEmitters.get(memberId); | ||
|
||
//then | ||
assertThat(sseEmitter).isNull(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters