From c38c0f1f62163ff44d1fff89d6f28c269ed632d5 Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Thu, 15 May 2025 15:14:00 +0900 Subject: [PATCH 1/2] =?UTF-8?q?RINGUS-74=20feat:=20Notification=20Domain?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/Notification.java | 50 ++++++++++++++++++ .../domain/notification/NotificationType.java | 6 +++ .../ringus/infra/config/RedisConfig.java | 8 +++ .../notification/NotificationController.java | 25 +++++++++ .../NotificationEmitterRequest.java | 19 +++++++ .../NotificationEmitterService.java | 51 +++++++++++++++++++ .../notification/NotificationSender.java | 10 ++++ src/main/resources/application.yml | 4 ++ 8 files changed, 173 insertions(+) create mode 100644 src/main/java/es/princip/ringus/domain/notification/Notification.java create mode 100644 src/main/java/es/princip/ringus/domain/notification/NotificationType.java create mode 100644 src/main/java/es/princip/ringus/presentation/notification/NotificationController.java create mode 100644 src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java create mode 100644 src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java create mode 100644 src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java diff --git a/src/main/java/es/princip/ringus/domain/notification/Notification.java b/src/main/java/es/princip/ringus/domain/notification/Notification.java new file mode 100644 index 0000000..0a1f269 --- /dev/null +++ b/src/main/java/es/princip/ringus/domain/notification/Notification.java @@ -0,0 +1,50 @@ +package es.princip.ringus.domain.notification; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "notification") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notification { + + @Id @Column(name = "notification_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long title; + + // 알림 내용 + @Column(nullable = false) + private String content; + + // 알림 유형 (ex: 멘토 신청, 수락, 메시지 등) + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private NotificationType type; + + // 읽음 여부 + @Column(nullable = false) + private boolean isRead = false; + + // 수신자 / member 이랑 many to one? + @Column(name = "receiver_id", nullable = false) + private Long receiverId; + + @Builder + public Notification(String content, NotificationType type, Long receiverId) { + this.content = content; + this.type = type; + this.receiverId = receiverId; + } + + public void markAsRead() { + this.isRead = true; + } +} + diff --git a/src/main/java/es/princip/ringus/domain/notification/NotificationType.java b/src/main/java/es/princip/ringus/domain/notification/NotificationType.java new file mode 100644 index 0000000..5d50513 --- /dev/null +++ b/src/main/java/es/princip/ringus/domain/notification/NotificationType.java @@ -0,0 +1,6 @@ +package es.princip.ringus.domain.notification; + +public enum NotificationType { + MENTORING_REQUEST, + MENTORING_APPROVED +} diff --git a/src/main/java/es/princip/ringus/infra/config/RedisConfig.java b/src/main/java/es/princip/ringus/infra/config/RedisConfig.java index fffdd27..7b3c021 100644 --- a/src/main/java/es/princip/ringus/infra/config/RedisConfig.java +++ b/src/main/java/es/princip/ringus/infra/config/RedisConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -30,4 +31,11 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } + + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } } \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationController.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationController.java new file mode 100644 index 0000000..64cd970 --- /dev/null +++ b/src/main/java/es/princip/ringus/presentation/notification/NotificationController.java @@ -0,0 +1,25 @@ +package es.princip.ringus.presentation.notification; + +import es.princip.ringus.global.util.ApiResponseWrapper; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +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; + +@RestController +@RequestMapping("/notifications") +@RequiredArgsConstructor +public class NotificationController { + + private final NotificationEmitterService notificationEmitterService; + + @GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public ResponseEntity> subscribe() { + SseEmitter response = null; + return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "연결 성공", response)); + } +} diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java new file mode 100644 index 0000000..122a22c --- /dev/null +++ b/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java @@ -0,0 +1,19 @@ +package es.princip.ringus.presentation.notification; + +import es.princip.ringus.domain.notification.Notification; + +public record NotificationEmitterRequest ( + Long senderId, + Long title, + String content, + Long receiverId +) { + public static NotificationEmitterRequest from(final Notification notification) { + return new NotificationEmitterRequest( + notification.getId(), + notification.getTitle(), + notification.getContent(), + notification.getReceiverId() + ); + } +} diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java new file mode 100644 index 0000000..2290d25 --- /dev/null +++ b/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java @@ -0,0 +1,51 @@ +package es.princip.ringus.presentation.notification; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class NotificationEmitterService implements NotificationSender { + @Value("${app.notification.emitter.timeout}") + private Long TIMEOUT; + + private final Map emitters = new ConcurrentHashMap<>(); + + public SseEmitter subscribe(Long memberId) { + SseEmitter emitter = new SseEmitter(TIMEOUT); + emitters.put(memberId, emitter); + + emitter.onCompletion(() -> emitters.remove(memberId)); + emitter.onTimeout(() -> emitters.remove(memberId)); + + try { + emitter.send(SseEmitter.event() + .name("connected") + .data("SSE 연결 완료")); + } catch (IOException e) { + emitter.completeWithError(e); + } + + return emitter; + } + + @Async + public void sendNotification(NotificationEmitterRequest notification) { + SseEmitter emitter = emitters.get(notification.receiverId()); + if (emitter != null) { + try { + emitter.send(SseEmitter.event() + .name("notification") + .data(notification)); + } catch (IOException e) { + emitter.completeWithError(e); + emitters.remove(notification.receiverId()); + } + } + } +} diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java new file mode 100644 index 0000000..a37fdeb --- /dev/null +++ b/src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java @@ -0,0 +1,10 @@ +package es.princip.ringus.presentation.notification; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface NotificationSender { + + public SseEmitter subscribe(Long memberId); + + public void sendNotification(NotificationEmitterRequest notification); +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index af76a90..65e6336 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -64,3 +64,7 @@ springdoc: swagger-ui: path: /swagger +app: + notification: + emitter: + timeout: ${NOTIFICATION_EMITTER_TIMEOUT} From b93a800ba1c1ec315ca4af3a77bc3758478afbeb Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 26 May 2025 16:26:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?RINGUS-84=20feat:=20=EB=A9=98=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EC=8B=A0=EC=B2=AD=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 8 +-- .../mentoring/MentoringService.java | 7 ++- .../service/NotificationService.java | 26 ++++++++ .../domain/notification/Notification.java | 41 +++++++------ .../notification/NotificationRepository.java | 6 ++ .../factory/NotificationMessageFactory.java | 28 +++++++++ .../global/sender/EmitterRepository.java | 40 ++++++++++++ .../global/sender/NotificationChannel.java | 8 +++ .../global/sender/NotificationSender.java | 8 +++ .../global/sender/SseNotificationSender.java | 36 +++++++++++ .../sender/dto/MentoringRequestMessage.java | 35 +++++++++++ .../notification/NotificationController.java | 61 +++++++++++++++---- .../NotificationEmitterRequest.java | 19 ------ .../NotificationEmitterService.java | 51 ---------------- .../notification/NotificationSender.java | 10 --- .../db/migration/V9__create_notification.sql | 21 +++++++ 16 files changed, 291 insertions(+), 114 deletions(-) create mode 100644 src/main/java/es/princip/ringus/application/notification/service/NotificationService.java create mode 100644 src/main/java/es/princip/ringus/domain/notification/NotificationRepository.java create mode 100644 src/main/java/es/princip/ringus/global/factory/NotificationMessageFactory.java create mode 100644 src/main/java/es/princip/ringus/global/sender/EmitterRepository.java create mode 100644 src/main/java/es/princip/ringus/global/sender/NotificationChannel.java create mode 100644 src/main/java/es/princip/ringus/global/sender/NotificationSender.java create mode 100644 src/main/java/es/princip/ringus/global/sender/SseNotificationSender.java create mode 100644 src/main/java/es/princip/ringus/global/sender/dto/MentoringRequestMessage.java delete mode 100644 src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java delete mode 100644 src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java delete mode 100644 src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java create mode 100644 src/main/resources/db/migration/V9__create_notification.sql diff --git a/docker-compose.yml b/docker-compose.yml index 60f2710..a87fed0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: - redis ports: - ${SPRING_PORT}:${SPRING_PORT} - restart: always + restart: "always" database: image: mysql:8.4.4 @@ -27,7 +27,7 @@ services: - ${DB_PORT} ports: - ${DB_PORT}:${DB_PORT} - restart: no + restart: "no" volumes: - ring-us-database:/var/lib/mysql @@ -39,9 +39,9 @@ services: - ${REDIS_PORT} ports: - ${REDIS_PORT}:${REDIS_PORT} - restart: always + restart: "always" volumes: - ring-us-redis:/data volumes: ring-us-database: - ring-us-redis: + ring-us-redis: \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/application/mentoring/MentoringService.java b/src/main/java/es/princip/ringus/application/mentoring/MentoringService.java index 0342dd5..14c8cff 100644 --- a/src/main/java/es/princip/ringus/application/mentoring/MentoringService.java +++ b/src/main/java/es/princip/ringus/application/mentoring/MentoringService.java @@ -1,5 +1,6 @@ package es.princip.ringus.application.mentoring; +import es.princip.ringus.application.notification.service.NotificationService; import es.princip.ringus.domain.exception.MenteeErrorCode; import es.princip.ringus.domain.exception.MentorErrorCode; import es.princip.ringus.domain.exception.MentoringErrorCode; @@ -11,6 +12,7 @@ import es.princip.ringus.domain.mentoring.MentoringRepository; import es.princip.ringus.domain.mentoring.MentoringStatus; import es.princip.ringus.global.exception.CustomRuntimeException; +import es.princip.ringus.global.sender.dto.MentoringRequestMessage; import es.princip.ringus.presentation.mentoring.dto.*; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -24,6 +26,7 @@ public class MentoringService { private final MentorRepository mentorRepository; private final MenteeRepository menteeRepository; + private final NotificationService notificationService; /** * 멘토링 신청 생성 */ @@ -39,11 +42,13 @@ public MentoringResponse createMentoring(CreateMentoringRequest request, Long me request.applyTimes(), request.mentoringMessage(), mentor, - mentee); + mentee + ); mentee.addMentoring(mentoring); mentor.addMentoring(mentoring); + notificationService.notify(MentoringRequestMessage.from(mentee, mentor, mentoring)); return MentoringResponse.from(mentoringRepository.save(mentoring)); } diff --git a/src/main/java/es/princip/ringus/application/notification/service/NotificationService.java b/src/main/java/es/princip/ringus/application/notification/service/NotificationService.java new file mode 100644 index 0000000..95aaf00 --- /dev/null +++ b/src/main/java/es/princip/ringus/application/notification/service/NotificationService.java @@ -0,0 +1,26 @@ +package es.princip.ringus.application.notification.service; + +import es.princip.ringus.domain.notification.Notification; +import es.princip.ringus.domain.notification.NotificationRepository; +import es.princip.ringus.global.factory.NotificationMessageFactory; +import es.princip.ringus.global.sender.NotificationSender; +import es.princip.ringus.global.sender.dto.MentoringRequestMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class NotificationService { + + private final NotificationSender notificationSender; + private final NotificationMessageFactory notificationMessageFactory; + private final NotificationRepository notificationRepository; + + public void notify(MentoringRequestMessage request) { + Notification notification = notificationMessageFactory.mentoringRequestMessage(request); + notificationRepository.save(notification); + notificationSender.send(notification); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/domain/notification/Notification.java b/src/main/java/es/princip/ringus/domain/notification/Notification.java index 0a1f269..8b35ca6 100644 --- a/src/main/java/es/princip/ringus/domain/notification/Notification.java +++ b/src/main/java/es/princip/ringus/domain/notification/Notification.java @@ -1,5 +1,6 @@ package es.princip.ringus.domain.notification; +import es.princip.ringus.domain.base.BaseTimeEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -10,41 +11,45 @@ @Entity @Table(name = "notification") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Notification { +public class Notification extends BaseTimeEntity { - @Id @Column(name = "notification_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_id") private Long id; - @Column(nullable = false) - private Long title; + @Column(name = "title", nullable = false, length = 255) + private String title; - // 알림 내용 - @Column(nullable = false) + @Column(name = "content", nullable = false, length = 500) private String content; - // 알림 유형 (ex: 멘토 신청, 수락, 메시지 등) @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(name = "type", nullable = false) private NotificationType type; - // 읽음 여부 - @Column(nullable = false) + @Column(name = "is_read", nullable = false) private boolean isRead = false; - // 수신자 / member 이랑 many to one? + @Column(name = "sender_id", nullable = false) + private Long senderId; + @Column(name = "receiver_id", nullable = false) private Long receiverId; @Builder - public Notification(String content, NotificationType type, Long receiverId) { + private Notification( + String title, + String content, + NotificationType type, + Long senderId, + Long receiverId + ) { + this.title = title; this.content = content; this.type = type; + this.senderId = senderId; this.receiverId = receiverId; } - public void markAsRead() { - this.isRead = true; - } -} - + public void markAsRead() { this.isRead = true; } +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/domain/notification/NotificationRepository.java b/src/main/java/es/princip/ringus/domain/notification/NotificationRepository.java new file mode 100644 index 0000000..ec6823f --- /dev/null +++ b/src/main/java/es/princip/ringus/domain/notification/NotificationRepository.java @@ -0,0 +1,6 @@ +package es.princip.ringus.domain.notification; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NotificationRepository extends JpaRepository { +} diff --git a/src/main/java/es/princip/ringus/global/factory/NotificationMessageFactory.java b/src/main/java/es/princip/ringus/global/factory/NotificationMessageFactory.java new file mode 100644 index 0000000..b7a2699 --- /dev/null +++ b/src/main/java/es/princip/ringus/global/factory/NotificationMessageFactory.java @@ -0,0 +1,28 @@ +package es.princip.ringus.global.factory; + +import es.princip.ringus.domain.notification.Notification; +import es.princip.ringus.domain.notification.NotificationType; +import es.princip.ringus.global.sender.dto.MentoringRequestMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NotificationMessageFactory { + + public Notification mentoringRequestMessage(MentoringRequestMessage request) { + String title = request.menteeName() + " 멘티님께서 " + request.mentorName() + " 멘토님께 멘토링을 신청했습니다."; + String content = "[링어스 멘토링 신청 알림]\n" + + "멘토링 주제" + request.mentoringTopic().name() + "\n" + + "신청 시간: " + request.applyTimes().toString() + "\n" + + "멘토링 신청 메시지: " + request.mentoringMessage() + "\n"+ + "\n\n"; + return Notification.builder() + .title(title) + .content(content) + .type(NotificationType.MENTORING_REQUEST) + .senderId(request.senderId()) + .receiverId(request.receiverId()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/global/sender/EmitterRepository.java b/src/main/java/es/princip/ringus/global/sender/EmitterRepository.java new file mode 100644 index 0000000..3b2cace --- /dev/null +++ b/src/main/java/es/princip/ringus/global/sender/EmitterRepository.java @@ -0,0 +1,40 @@ +package es.princip.ringus.global.sender; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +@RequiredArgsConstructor +public class EmitterRepository { + private final Map emitters = new ConcurrentHashMap<>(); + @Value("${app.notification.emitter.timeout}") + private Long TIMEOUT; + + public SseEmitter save(Long receiverId) { + log.info("Sending message to emitter {}", receiverId); + log.info("Emitter timeout set to {} ms", TIMEOUT); + + SseEmitter emitter = new SseEmitter(TIMEOUT); + emitters.put(receiverId, emitter); + + emitter.onCompletion(() -> emitters.remove(receiverId)); + emitter.onTimeout(() -> emitters.remove(receiverId)); + return emitter; + } + + public Optional get(Long receiverId) { + return Optional.ofNullable(emitters.get(receiverId)); + } + + public void remove(Long receiverId) { + emitters.remove(receiverId); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/global/sender/NotificationChannel.java b/src/main/java/es/princip/ringus/global/sender/NotificationChannel.java new file mode 100644 index 0000000..7a019b9 --- /dev/null +++ b/src/main/java/es/princip/ringus/global/sender/NotificationChannel.java @@ -0,0 +1,8 @@ +package es.princip.ringus.global.sender; + +public enum NotificationChannel { + SSE, + EMAIL, + KAKAO, + SMS +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/global/sender/NotificationSender.java b/src/main/java/es/princip/ringus/global/sender/NotificationSender.java new file mode 100644 index 0000000..6b2c00c --- /dev/null +++ b/src/main/java/es/princip/ringus/global/sender/NotificationSender.java @@ -0,0 +1,8 @@ +package es.princip.ringus.global.sender; + +import es.princip.ringus.domain.notification.Notification; + +public interface NotificationSender { + void send(Notification notification); + NotificationChannel getChannelType(); +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/global/sender/SseNotificationSender.java b/src/main/java/es/princip/ringus/global/sender/SseNotificationSender.java new file mode 100644 index 0000000..8545f38 --- /dev/null +++ b/src/main/java/es/princip/ringus/global/sender/SseNotificationSender.java @@ -0,0 +1,36 @@ +package es.princip.ringus.global.sender; + +import es.princip.ringus.domain.notification.Notification; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class SseNotificationSender implements NotificationSender { + + private final EmitterRepository emitterRepository; + + @Override + public NotificationChannel getChannelType() { + return NotificationChannel.SSE; + } + + @Override + public void send(Notification notification) { + emitterRepository.get(notification.getReceiverId()).ifPresent(emitter -> { + try { + emitter.send( + SseEmitter.event() + .name("notification") + .data(notification) // 직렬화 규칙은 Jackson 기본 + ); + } catch (IOException ex) { + emitter.completeWithError(ex); + emitterRepository.remove(notification.getReceiverId()); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/global/sender/dto/MentoringRequestMessage.java b/src/main/java/es/princip/ringus/global/sender/dto/MentoringRequestMessage.java new file mode 100644 index 0000000..92ac204 --- /dev/null +++ b/src/main/java/es/princip/ringus/global/sender/dto/MentoringRequestMessage.java @@ -0,0 +1,35 @@ +package es.princip.ringus.global.sender.dto; + +import es.princip.ringus.domain.mentee.Mentee; +import es.princip.ringus.domain.mentor.Mentor; +import es.princip.ringus.domain.mentoring.Mentoring; +import es.princip.ringus.domain.mentoring.MentoringTime; +import es.princip.ringus.domain.mentoring.MentoringTopic; + +import java.util.List; + +public record MentoringRequestMessage( + Long receiverId, + Long senderId, + String menteeName, + String mentorName, + String mentoringMessage, + MentoringTopic mentoringTopic, + List applyTimes +) { + public static MentoringRequestMessage from( + final Mentee mentee, + final Mentor mentor, + final Mentoring mentoring + ) { + return new MentoringRequestMessage( + mentor.getMemberId(), + mentee.getMemberId(), + mentee.getNickname(), + mentor.getNickname(), + mentoring.getMentoringMessage(), + mentoring.getMentoringTopic(), + mentoring.getApplyTimes() + ); + } +} diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationController.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationController.java index 64cd970..b4e2f59 100644 --- a/src/main/java/es/princip/ringus/presentation/notification/NotificationController.java +++ b/src/main/java/es/princip/ringus/presentation/notification/NotificationController.java @@ -1,25 +1,64 @@ package es.princip.ringus.presentation.notification; -import es.princip.ringus.global.util.ApiResponseWrapper; +import es.princip.ringus.domain.notification.Notification; +import es.princip.ringus.domain.notification.NotificationType; +import es.princip.ringus.global.annotation.SessionCheck; +import es.princip.ringus.global.annotation.SessionMemberId; +import es.princip.ringus.global.sender.EmitterRepository; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -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.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.io.IOException; + @RestController @RequestMapping("/notifications") @RequiredArgsConstructor public class NotificationController { - private final NotificationEmitterService notificationEmitterService; + private final EmitterRepository emitterRepository; + + @GetMapping("/{receiverId}") + public void testSend(@PathVariable Long receiverId) { + Notification n = Notification.builder() + .title("테스트 알림") + .content("Postman SSE 테스트입니다.") + .receiverId(receiverId) + .type(NotificationType.MENTORING_APPROVED) + .build(); + try { + emitterRepository.get(receiverId).ifPresent(emitter -> { + try { + emitter.send( + SseEmitter.event() + .name("notification") + .data(n) // 직렬화 규칙은 Jackson 기본 + ); + } catch (IOException e) { + emitter.completeWithError(e); + emitterRepository.remove(receiverId); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + @SessionCheck @GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public ResponseEntity> subscribe() { - SseEmitter response = null; - return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "연결 성공", response)); + public SseEmitter subscribe(@SessionMemberId Long memberId) { + SseEmitter emitter = emitterRepository.save(memberId); + try { + emitter.send( + SseEmitter.event() + .name("connected") + .data("SSE 연결 완료") + ); + } catch (IOException e) { + emitter.completeWithError(e); + emitterRepository.remove(memberId); + } + return emitter; } -} +} \ No newline at end of file diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java deleted file mode 100644 index 122a22c..0000000 --- a/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package es.princip.ringus.presentation.notification; - -import es.princip.ringus.domain.notification.Notification; - -public record NotificationEmitterRequest ( - Long senderId, - Long title, - String content, - Long receiverId -) { - public static NotificationEmitterRequest from(final Notification notification) { - return new NotificationEmitterRequest( - notification.getId(), - notification.getTitle(), - notification.getContent(), - notification.getReceiverId() - ); - } -} diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java deleted file mode 100644 index 2290d25..0000000 --- a/src/main/java/es/princip/ringus/presentation/notification/NotificationEmitterService.java +++ /dev/null @@ -1,51 +0,0 @@ -package es.princip.ringus.presentation.notification; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Component -public class NotificationEmitterService implements NotificationSender { - @Value("${app.notification.emitter.timeout}") - private Long TIMEOUT; - - private final Map emitters = new ConcurrentHashMap<>(); - - public SseEmitter subscribe(Long memberId) { - SseEmitter emitter = new SseEmitter(TIMEOUT); - emitters.put(memberId, emitter); - - emitter.onCompletion(() -> emitters.remove(memberId)); - emitter.onTimeout(() -> emitters.remove(memberId)); - - try { - emitter.send(SseEmitter.event() - .name("connected") - .data("SSE 연결 완료")); - } catch (IOException e) { - emitter.completeWithError(e); - } - - return emitter; - } - - @Async - public void sendNotification(NotificationEmitterRequest notification) { - SseEmitter emitter = emitters.get(notification.receiverId()); - if (emitter != null) { - try { - emitter.send(SseEmitter.event() - .name("notification") - .data(notification)); - } catch (IOException e) { - emitter.completeWithError(e); - emitters.remove(notification.receiverId()); - } - } - } -} diff --git a/src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java b/src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java deleted file mode 100644 index a37fdeb..0000000 --- a/src/main/java/es/princip/ringus/presentation/notification/NotificationSender.java +++ /dev/null @@ -1,10 +0,0 @@ -package es.princip.ringus.presentation.notification; - -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -public interface NotificationSender { - - public SseEmitter subscribe(Long memberId); - - public void sendNotification(NotificationEmitterRequest notification); -} diff --git a/src/main/resources/db/migration/V9__create_notification.sql b/src/main/resources/db/migration/V9__create_notification.sql new file mode 100644 index 0000000..76798f7 --- /dev/null +++ b/src/main/resources/db/migration/V9__create_notification.sql @@ -0,0 +1,21 @@ +CREATE TABLE notification ( + notification_id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + content VARCHAR(500) NOT NULL, + type ENUM ( + 'MENTORING_REQUEST', + 'MENTORING_APPROVED', + 'MENTORING_REJECTED' + ) NOT NULL, + is_read TINYINT(1) NOT NULL DEFAULT 0, + sender_id BIGINT NOT NULL, + receiver_id BIGINT NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + + PRIMARY KEY (notification_id), + CONSTRAINT fk_notification_sender + FOREIGN KEY (sender_id) REFERENCES member(member_id) ON DELETE CASCADE, + CONSTRAINT fk_notification_receiver + FOREIGN KEY (receiver_id) REFERENCES member(member_id) ON DELETE CASCADE +); \ No newline at end of file