Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
- redis
ports:
- ${SPRING_PORT}:${SPRING_PORT}
restart: always
restart: "always"

database:
image: mysql:8.4.4
Expand All @@ -27,7 +27,7 @@ services:
- ${DB_PORT}
ports:
- ${DB_PORT}:${DB_PORT}
restart: no
restart: "no"
volumes:
- ring-us-database:/var/lib/mysql

Expand All @@ -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:
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -24,6 +26,7 @@ public class MentoringService {
private final MentorRepository mentorRepository;
private final MenteeRepository menteeRepository;

private final NotificationService notificationService;
/**
* 멘토링 신청 생성
*/
Expand All @@ -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));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package es.princip.ringus.domain.notification;

import es.princip.ringus.domain.base.BaseTimeEntity;
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 extends BaseTimeEntity {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "notification_id")
private Long id;

@Column(name = "title", nullable = false, length = 255)
private String title;

@Column(name = "content", nullable = false, length = 500)
private String content;

@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false)
private NotificationType type;

@Column(name = "is_read", nullable = false)
private boolean isRead = false;

@Column(name = "sender_id", nullable = false)
private Long senderId;

@Column(name = "receiver_id", nullable = false)
private Long receiverId;

@Builder
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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package es.princip.ringus.domain.notification;

import org.springframework.data.jpa.repository.JpaRepository;

public interface NotificationRepository extends JpaRepository<Notification, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package es.princip.ringus.domain.notification;

public enum NotificationType {
MENTORING_REQUEST,
MENTORING_APPROVED
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Long, SseEmitter> 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<SseEmitter> get(Long receiverId) {
return Optional.ofNullable(emitters.get(receiverId));
}

public void remove(Long receiverId) {
emitters.remove(receiverId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package es.princip.ringus.global.sender;

public enum NotificationChannel {
SSE,
EMAIL,
KAKAO,
SMS
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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());
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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<MentoringTime> 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()
);
}
}
8 changes: 8 additions & 0 deletions src/main/java/es/princip/ringus/infra/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,4 +31,11 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connec
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
Loading