diff --git a/build.gradle b/build.gradle index 0e61fa5..e34ef7e 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-aws-messaging:2.2.4.RELEASE' implementation 'org.springframework.kafka:spring-kafka' implementation 'software.amazon.awssdk:sns:2.21.37' - implementation 'io.github.lotteon-maven:blooming-blooms-utils:25057818' + implementation 'io.github.lotteon-maven:blooming-blooms-utils:202312101522' testImplementation 'org.mockito:mockito-core:4.8.0' diff --git a/src/main/java/kr/bb/notification/domain/emitter/api/SSEClientController.java b/src/main/java/kr/bb/notification/domain/emitter/api/SSEClientController.java index d629afe..cbba542 100644 --- a/src/main/java/kr/bb/notification/domain/emitter/api/SSEClientController.java +++ b/src/main/java/kr/bb/notification/domain/emitter/api/SSEClientController.java @@ -1,7 +1,9 @@ package kr.bb.notification.domain.emitter.api; import kr.bb.notification.domain.emitter.application.SseService; +import kr.bb.notification.domain.notification.entity.NotificationCommand.SSENotification; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -12,8 +14,16 @@ public class SSEClientController { private final SseService sseService; - @PostMapping("/send-data/{userId}") - public void sendData(@PathVariable Long userId, @RequestBody Object data) { - sseService.notify(userId, data); + // TODO: 삭제될 예정, sse 전송 테스트 용 + @PostMapping(value = "/send-data/manager/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public void sendData(@PathVariable Long userId, @RequestBody String data) { + SSENotification build = + SSENotification.builder() + .role("manager") + .redirectUrl("/question") + .content(data) + .userId(3L) + .build(); + sseService.notify(build); } } diff --git a/src/main/java/kr/bb/notification/domain/emitter/api/SSERestController.java b/src/main/java/kr/bb/notification/domain/emitter/api/SSERestController.java index 0bc82fa..a9f7ea9 100644 --- a/src/main/java/kr/bb/notification/domain/emitter/api/SSERestController.java +++ b/src/main/java/kr/bb/notification/domain/emitter/api/SSERestController.java @@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -13,8 +13,9 @@ public class SSERestController { private final SseService sseService; - @GetMapping(value = "subscribe/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter subscribe(@RequestHeader Long userId) { - return sseService.subscribe(userId); + // TODO: 화면 테스트용으로 pathvariable 사용 + @GetMapping(value = "subscribe/manager/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribeManager(@PathVariable Long userId) { + return sseService.subscribe(userId, "manager"); } } diff --git a/src/main/java/kr/bb/notification/domain/emitter/application/SseService.java b/src/main/java/kr/bb/notification/domain/emitter/application/SseService.java index 0f7e904..0360dae 100644 --- a/src/main/java/kr/bb/notification/domain/emitter/application/SseService.java +++ b/src/main/java/kr/bb/notification/domain/emitter/application/SseService.java @@ -1,47 +1,70 @@ package kr.bb.notification.domain.emitter.application; +import bloomingblooms.domain.notification.NotificationKind; import java.io.IOException; import kr.bb.notification.domain.emitter.repository.SSERepository; +import kr.bb.notification.domain.notification.entity.NotificationCommand.SSENotification; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +@Slf4j @Service @RequiredArgsConstructor public class SseService { private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; private final SSERepository emitterRepository; - public SseEmitter subscribe(Long userId) { - SseEmitter emitter = createEmitter(userId); - sendToClient(userId, "EventStream Created. [userId=" + userId + "]"); + public SseEmitter subscribe(Long userId, String role) { + SseEmitter emitter = createEmitter(userId, role); + sendToClient(userId, role, "EventStream Created. [userId=" + userId + "]"); return emitter; } - public void notify(Long userId, Object event) { - sendToClient(userId, event); + public void notify(SSENotification event) { + sendToClient(event); } - private void sendToClient(Long userId, Object data) { - SseEmitter emitter = emitterRepository.get(userId); + private void sendToClient(SSENotification event) { + String id = event.getRole() + event.getUserId(); + String data = event.getContent() + "\n" + event.getRedirectUrl(); + SseEmitter emitter = emitterRepository.get(event.getUserId(), event.getRole()); + if (emitter != null) { + try { + emitter.send( + SseEmitter.event() + .id(id) + .name(NotificationKind.QUESTION.getKind()) + .data(data, MediaType.TEXT_EVENT_STREAM)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + private void sendToClient(Long userId, String role, Object data) { + SseEmitter emitter = emitterRepository.get(userId, role); + if (emitter != null) { try { emitter.send(SseEmitter.event().id(String.valueOf(userId)).name("sse").data(data)); } catch (IOException exception) { - emitterRepository.deleteById(userId); + emitterRepository.deleteById(userId, role); throw new RuntimeException("연결 오류!"); } } } - private SseEmitter createEmitter(Long userId) { + private SseEmitter createEmitter(Long userId, String role) { SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); - emitterRepository.save(userId, emitter); + emitterRepository.save(userId, role, emitter); // Emitter가 완료될 때(모든 데이터가 성공적으로 전송된 상태) Emitter를 삭제한다. - emitter.onCompletion(() -> emitterRepository.deleteById(userId)); + emitter.onCompletion(() -> emitterRepository.deleteById(userId, role)); // Emitter가 타임아웃 되었을 때(지정된 시간동안 어떠한 이벤트도 전송되지 않았을 때) Emitter를 삭제한다. - emitter.onTimeout(() -> emitterRepository.deleteById(userId)); + emitter.onTimeout(() -> emitterRepository.deleteById(userId, role)); return emitter; } diff --git a/src/main/java/kr/bb/notification/domain/emitter/repository/SSERepository.java b/src/main/java/kr/bb/notification/domain/emitter/repository/SSERepository.java index 281b845..76cc9ff 100644 --- a/src/main/java/kr/bb/notification/domain/emitter/repository/SSERepository.java +++ b/src/main/java/kr/bb/notification/domain/emitter/repository/SSERepository.java @@ -9,17 +9,17 @@ @Repository @RequiredArgsConstructor public class SSERepository { - private final Map emitters = new ConcurrentHashMap<>(); + private final Map emitters = new ConcurrentHashMap<>(); - public void save(Long userId, SseEmitter emitter) { - emitters.put(userId, emitter); + public void save(Long userId, String role, SseEmitter emitter) { + emitters.put(role + userId, emitter); } - public void deleteById(Long userId) { - emitters.remove(userId); + public void deleteById(Long userId, String role) { + emitters.remove(role + userId); } - public SseEmitter get(Long userId) { - return emitters.get(userId); + public SseEmitter get(Long userId, String role) { + return emitters.get(role + userId); } } diff --git a/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java b/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java index d4f0f0b..5478294 100644 --- a/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java +++ b/src/main/java/kr/bb/notification/domain/notification/application/NotificationCommandService.java @@ -1,6 +1,7 @@ package kr.bb.notification.domain.notification.application; import bloomingblooms.domain.notification.NotificationData; +import bloomingblooms.domain.notification.QuestionRegisterNotification; import bloomingblooms.domain.resale.ResaleNotificationList; import java.util.List; import kr.bb.notification.domain.notification.entity.MemberNotification; @@ -19,10 +20,21 @@ public class NotificationCommandService { @Transactional public void saveResaleNotification(NotificationData restoreNotification) { - Notification notification = NotificationCommand.toEntity(restoreNotification.getMessage()); + Notification notification = NotificationCommand.toEntity(restoreNotification.getPublishInformation()); List memberNotifications = MemberNotificationCommand.toEntityList(restoreNotification.getWhoToNotify()); notification.setMemberNotifications(memberNotifications); notificationJpaRepository.save(notification); } + + @Transactional + public void saveQuestionRegister( + NotificationData questionRegisterNotification) { + Notification notification = + NotificationCommand.toEntity(questionRegisterNotification.getPublishInformation()); + MemberNotification memberNotification = + MemberNotificationCommand.toEntity(questionRegisterNotification.getWhoToNotify()); + notification.setMemberNotifications(List.of(memberNotification)); + notificationJpaRepository.save(notification); + } } diff --git a/src/main/java/kr/bb/notification/domain/notification/entity/MemberNotificationCommand.java b/src/main/java/kr/bb/notification/domain/notification/entity/MemberNotificationCommand.java index 5bef6c6..47531b8 100644 --- a/src/main/java/kr/bb/notification/domain/notification/entity/MemberNotificationCommand.java +++ b/src/main/java/kr/bb/notification/domain/notification/entity/MemberNotificationCommand.java @@ -1,5 +1,6 @@ package kr.bb.notification.domain.notification.entity; +import bloomingblooms.domain.notification.QuestionRegisterNotification; import bloomingblooms.domain.resale.ResaleNotificationList; import java.util.List; import java.util.stream.Collectors; @@ -11,4 +12,8 @@ public static List toEntityList(ResaleNotificationList whoTo .map(item -> MemberNotification.builder().userId(item.getUserId()).build()) .collect(Collectors.toList()); } + + public static MemberNotification toEntity(QuestionRegisterNotification whoToNotify) { + return MemberNotification.builder().userId(whoToNotify.getUserId()).build(); + } } diff --git a/src/main/java/kr/bb/notification/domain/notification/entity/NotificationCommand.java b/src/main/java/kr/bb/notification/domain/notification/entity/NotificationCommand.java index 2b25624..4793d68 100644 --- a/src/main/java/kr/bb/notification/domain/notification/entity/NotificationCommand.java +++ b/src/main/java/kr/bb/notification/domain/notification/entity/NotificationCommand.java @@ -2,6 +2,7 @@ import bloomingblooms.domain.notification.NotificationData; import bloomingblooms.domain.notification.PublishNotificationInformation; +import bloomingblooms.domain.notification.QuestionRegisterNotification; import bloomingblooms.domain.resale.ResaleNotificationList; import java.util.List; import java.util.stream.Collectors; @@ -58,7 +59,7 @@ public SMSNotification(Long userId, String content, String redirectUrl, String p this.phoneNumber = phoneNumber; } - public static List getData( + public static List getResaleNotificationSMSData( NotificationData restoreNotification) { return restoreNotification.getWhoToNotify().getResaleNotificationData().stream() .map( @@ -66,10 +67,10 @@ public static List getData( SMSNotification.builder() .phoneNumber(item.getPhoneNumber()) .content( - restoreNotification.getMessage().getNotificationKind().getKind() + restoreNotification.getPublishInformation().getNotificationKind().getKind() + "\n" + restoreNotification.getWhoToNotify().getProductName() - + restoreNotification.getMessage().getMessage()) + + restoreNotification.getPublishInformation().getMessage()) .redirectUrl( RedirectUrl.PRODUCT_DETAIL.getRedirectUrl() + restoreNotification.getWhoToNotify().getProductId()) @@ -96,4 +97,38 @@ public SMSNotification build() { } } } + + @Getter + public static class SSENotification extends NotificationInformation { + private final String role; + + @Builder + public SSENotification(Long userId, String content, String redirectUrl, String role) { + super(userId, content, redirectUrl); + this.role = role; + } + + public static SSENotificationBuilder builder() { + return new SSENotificationBuilder(); + } + + public static SSENotification getQuestionRegisterSSEData( + NotificationData questionRegisterNotification) { + return SSENotification.builder() + .content( + questionRegisterNotification.getPublishInformation().getNotificationKind().getKind() + + "\n" + + questionRegisterNotification.getPublishInformation().getMessage()) + .redirectUrl(questionRegisterNotification.getPublishInformation().getNotificationUrl()) + .role(questionRegisterNotification.getWhoToNotify().getRole().getRole()) + .build(); + } + + public static class SSENotificationBuilder extends NotificationInformationBuilder { + @Override + public SSENotification build() { + return new SSENotification(userId, content, redirectUrl, role); + } + } + } } diff --git a/src/main/java/kr/bb/notification/domain/notification/facade/ResaleNotificationFacadeHandler.java b/src/main/java/kr/bb/notification/domain/notification/facade/NotificationFacadeHandler.java similarity index 53% rename from src/main/java/kr/bb/notification/domain/notification/facade/ResaleNotificationFacadeHandler.java rename to src/main/java/kr/bb/notification/domain/notification/facade/NotificationFacadeHandler.java index 0265c70..fbd992d 100644 --- a/src/main/java/kr/bb/notification/domain/notification/facade/ResaleNotificationFacadeHandler.java +++ b/src/main/java/kr/bb/notification/domain/notification/facade/NotificationFacadeHandler.java @@ -1,26 +1,40 @@ package kr.bb.notification.domain.notification.facade; import bloomingblooms.domain.notification.NotificationData; +import bloomingblooms.domain.notification.QuestionRegisterNotification; import bloomingblooms.domain.resale.ResaleNotificationList; import java.util.List; import kr.bb.notification.domain.notification.application.NotificationCommandService; import kr.bb.notification.domain.notification.entity.NotificationCommand.SMSNotification; +import kr.bb.notification.domain.notification.entity.NotificationCommand.SSENotification; import kr.bb.notification.domain.notification.infrastructure.sms.SendSMS; +import kr.bb.notification.domain.notification.infrastructure.sse.SendSSE; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor -public class ResaleNotificationFacadeHandler { +public class NotificationFacadeHandler { private final SendSMS sms; + private final SendSSE sse; private final NotificationCommandService notificationCommandService; public void publishResaleNotification( NotificationData restoreNotification) { - List data = SMSNotification.getData(restoreNotification); + List data = SMSNotification.getResaleNotificationSMSData(restoreNotification); data.forEach(sms::publishCustomer); // save notification notificationCommandService.saveResaleNotification(restoreNotification); } + + public void publishQuestionRegisterNotification( + NotificationData questionRegisterNotification) { + SSENotification sseNotification = + SSENotification.getQuestionRegisterSSEData(questionRegisterNotification); + sse.publishCustomer(sseNotification); + + // save notification + notificationCommandService.saveQuestionRegister(questionRegisterNotification); + } } diff --git a/src/main/java/kr/bb/notification/domain/notification/infrastructure/action/InfrastructureActionHandler.java b/src/main/java/kr/bb/notification/domain/notification/infrastructure/action/InfrastructureActionHandler.java index d6065bf..4bfe092 100644 --- a/src/main/java/kr/bb/notification/domain/notification/infrastructure/action/InfrastructureActionHandler.java +++ b/src/main/java/kr/bb/notification/domain/notification/infrastructure/action/InfrastructureActionHandler.java @@ -5,5 +5,5 @@ public interface InfrastructureActionHandler< T extends NotificationCommand.NotificationInformation> { - void publishCustomer(T NotifyData); + void publishCustomer(T notifyData); } diff --git a/src/main/java/kr/bb/notification/domain/notification/infrastructure/message/NotificationSQSListener.java b/src/main/java/kr/bb/notification/domain/notification/infrastructure/message/NotificationSQSListener.java index 6a0d8b5..b4580d6 100644 --- a/src/main/java/kr/bb/notification/domain/notification/infrastructure/message/NotificationSQSListener.java +++ b/src/main/java/kr/bb/notification/domain/notification/infrastructure/message/NotificationSQSListener.java @@ -1,11 +1,12 @@ package kr.bb.notification.domain.notification.infrastructure.message; import bloomingblooms.domain.notification.NotificationData; +import bloomingblooms.domain.notification.QuestionRegisterNotification; import bloomingblooms.domain.resale.ResaleNotificationList; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; -import kr.bb.notification.domain.notification.facade.ResaleNotificationFacadeHandler; +import kr.bb.notification.domain.notification.facade.NotificationFacadeHandler; import lombok.RequiredArgsConstructor; import org.springframework.cloud.aws.messaging.listener.Acknowledgment; import org.springframework.cloud.aws.messaging.listener.SqsMessageDeletionPolicy; @@ -18,7 +19,7 @@ @RequiredArgsConstructor public class NotificationSQSListener { private final ObjectMapper objectMapper; - private final ResaleNotificationFacadeHandler resaleNotificationFacadeHandler; + private final NotificationFacadeHandler notificationFacadeHandler; @SqsListener( value = "${cloud.aws.sqs.product-resale-notification-queue.name}", @@ -33,7 +34,25 @@ public void consumeProductResaleNotificationCheckQueue( .getTypeFactory() .constructParametricType(NotificationData.class, ResaleNotificationList.class)); // call facade - resaleNotificationFacadeHandler.publishResaleNotification(restoreNotification); + notificationFacadeHandler.publishResaleNotification(restoreNotification); + ack.acknowledge(); + } + + @SqsListener( + value = "${cloud.aws.sqs.question-register-notification-queue.name}", + deletionPolicy = SqsMessageDeletionPolicy.NEVER) + public void consumeQuestionRegisterNotificationQueue( + @Payload String message, @Headers Map headers, Acknowledgment ack) + throws JsonProcessingException { + NotificationData questionRegisterNotification = + objectMapper.readValue( + message, + objectMapper + .getTypeFactory() + .constructParametricType( + NotificationData.class, QuestionRegisterNotification.class)); + // call facade + notificationFacadeHandler.publishQuestionRegisterNotification(questionRegisterNotification); ack.acknowledge(); } } diff --git a/src/main/java/kr/bb/notification/domain/notification/infrastructure/sse/SendSSE.java b/src/main/java/kr/bb/notification/domain/notification/infrastructure/sse/SendSSE.java new file mode 100644 index 0000000..b98dc8e --- /dev/null +++ b/src/main/java/kr/bb/notification/domain/notification/infrastructure/sse/SendSSE.java @@ -0,0 +1,20 @@ +package kr.bb.notification.domain.notification.infrastructure.sse; + +import kr.bb.notification.domain.emitter.application.SseService; +import kr.bb.notification.domain.notification.entity.NotificationCommand.SSENotification; +import kr.bb.notification.domain.notification.infrastructure.action.InfrastructureActionHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SendSSE implements InfrastructureActionHandler { + private final SseService sseService; + + @Override + public void publishCustomer(SSENotification notifyData) { + sseService.notify(notifyData); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 29f138e..f10e833 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -30,16 +30,19 @@ cloud: region: static: ap-northeast-1 s3: - name: "fe36d29bf7af475147c1a455f405c4ebfad3038756bd787a6433d88c5110faa9" + name: "" sns: - arn: "76e6baa63a82bed2f6740546b8a6159c0eefa8034f7bf4ad270b0177a2a1e74e864fc7b02ca7d782e984431d02f5fbcbeeaf7bb65b765a95d6647ced163bfd0a60e54fc1e9f32f116c2c19ef982fe2e666f7cd142a752ba82d9946b2364267ca" + arn: "" credentials: - ACCESS_KEY_ID: "3a3790de8d6cef78bd1f85d4f49521309a0a239125fd3096a4d5cd71fb21e7f6181a403325589fb6f8e03abc47f769db" - SECRET_ACCESS_KEY: "aff21fe25a18c0f596f99e05793665f4a6e504c8cae0052eb85f683cac6c62fab40aa358965edfb6cecb0725a2ae55ffb00b8f378038b4d1166748f034fa2ec2" + ACCESS_KEY_ID: "test" + SECRET_ACCESS_KEY: "test" sqs: product-resale-notification-check-queue: - name: "3a56ae4ff2005888aaa1de21bd8a5fbd6ea200f32e6b7bf1f70c3afdcad7fd040a4a1354fbc99d1aa37f61c2a79c83a9b4c6f0621f3427cb42abfaca4029f3c1" - url: "effc4d4abcad66b717744ea6a3a0986052bdd924188b2b4806d7ef260f899ac9620d5ccb7a3bcbaafa944a52e29e4624f169e90d7b3f640539eede61c402e890c12bc771dde2c19b6d9bf215eb5f2784c049816806f5a90371d9e4a9541eb4bb407baa0f6c02a4082abb1b274062e760" + name: "" + url: "" product-resale-notification-queue: - name: "7771e617df13f290e098849be2fb284d9be204177a903da416c350c42e3522f462041ec6919269ddb063b4d8e56a3727c4807c7398482a617fefd6f985e29906" - url: "fc15218b226e1da082b9815bd50e85df528ca61efe0b13973604000156347e63ffa132cc6102bd2b16e1329fb587d2500d6959e24639582d470d745a6c955e0f2e3f60fe6906d3b34af4e2f1ba7eb7ef9f696f57a14331b49eaa3a53b624586204db65d8fce78463e948606ca9b2bbd0" + name: "" + url: "" + question-register-notification-queue: + name: "" + url: "" \ No newline at end of file