Skip to content

Commit

Permalink
Merge pull request #10 from urdego/Feature/#1
Browse files Browse the repository at this point in the history
Feature/#1 알림 기능 구현
  • Loading branch information
Ban-gilhyeon authored Feb 6, 2025
2 parents 1b7ebfc + 0162794 commit 2971b99
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ public RedisConnectionFactory redisConnectionFactory() {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
@Getter
@RequiredArgsConstructor
public enum ExceptionMessage {
NOT_FOUND_USER("해당 유저를 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "not found user");
NOT_FOUND_USER("해당 유저를 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "not found user"),
NOT_FOUND_NOTIFICATION("해당 알림을 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "not found notification"),
INVALID_NOTIFICATION_ID("잘못된 알림 ID 입니다.", HttpStatus.BAD_REQUEST, "invalid notification id");

private final String text;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package urdego.io.urdego_notification_service.common.exception.notification;

import urdego.io.urdego_notification_service.common.exception.BaseException;
import urdego.io.urdego_notification_service.common.exception.ExceptionMessage;

public class InvalidNotificationId extends BaseException {
public static final BaseException EXCEPTION = new InvalidNotificationId();

private InvalidNotificationId() {
super(ExceptionMessage.INVALID_NOTIFICATION_ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package urdego.io.urdego_notification_service.common.exception.notification;

import urdego.io.urdego_notification_service.common.exception.BaseException;
import urdego.io.urdego_notification_service.common.exception.ExceptionMessage;

public class NotFoundNotification extends BaseException {
public static final BaseException EXCEPTION = new NotFoundNotification();

private NotFoundNotification() {
super(ExceptionMessage.NOT_FOUND_NOTIFICATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,51 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import urdego.io.urdego_notification_service.controller.dto.request.notification.NotificationRequest;
import urdego.io.urdego_notification_service.controller.dto.WebSocketMessage;
import urdego.io.urdego_notification_service.controller.dto.request.ReplyRequest;
import urdego.io.urdego_notification_service.controller.dto.request.notification.NotificationRequest;
import urdego.io.urdego_notification_service.domain.entity.Notification;
import urdego.io.urdego_notification_service.domain.service.NotificationService;

import java.util.List;

@Controller
@RequiredArgsConstructor
@RequestMapping("/api/notification-service/notifications")
@RequestMapping("api/notification-service/notifications")
@Slf4j
public class NotificationController {
private final NotificationService notificationService;
//TODO : Response에 Entity를 리턴하는 행위는 좋지 않음 차후 NotificationResponse로 수정해야 함

@PostMapping("/send")
@MessageMapping("/send")
@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = WebSocketMessage.class)))
@Operation(summary = "게임초대 알림 전송",description = "userId로 게임초대 알림 전송")
public ResponseEntity<WebSocketMessage<Notification>> sendNotification(@RequestBody NotificationRequest request) {
//uri가 send로 나눌 필요가 있나? {userId}로 해도 되지 않나? or PK는 숨기는 좋을까?
WebSocketMessage<Notification> response = notificationService.publishNotification(request);
return ResponseEntity.ok().body(response);
}

@MessageMapping("/{userId}/reply")
@Operation(summary = "알림 답장", description = "notificationId로 게임초대 알림에 대한 답변 받기")
public ResponseEntity<Object> replyNotification(@DestinationVariable("userId") Long userId,
@RequestBody ReplyRequest request) {
Notification updatedNotification = notificationService.updateReadStatus(request, userId);
return ResponseEntity.ok().body(updatedNotification);
}

@GetMapping("/{userId}")
@Operation(summary = "알림 조회", description = "userId로 해당 유저에게 전달된 전체 알림 조회")
public ResponseEntity<List<Notification>> getNotifications(@PathVariable("userId") Long userId) {

List<Notification> notifications = notificationService.readNotificationList(userId);
return ResponseEntity.ok().body(notifications);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import urdego.io.urdego_notification_service.controller.dto.request.game.AnswerReq;
import urdego.io.urdego_notification_service.controller.dto.request.game.QuestionReq;
import urdego.io.urdego_notification_service.controller.dto.request.game.ScoreReq;
Expand Down Expand Up @@ -41,4 +40,4 @@ public interface GameServiceClient {

@PostMapping("/api/game-service/round/answer")
ResponseEntity<AnswerRes> submitAnswer(@RequestBody AnswerReq request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package urdego.io.urdego_notification_service.controller.dto.request;

import java.util.UUID;

public record ReplyRequest(
boolean isAccepted,
String notificationId
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package urdego.io.urdego_notification_service.domain.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -18,8 +19,9 @@
@NoArgsConstructor
@Component
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class Notification implements Serializable {
private UUID NotificationId;
private UUID notificationId;

private Long senderId;
private Long targetId;
Expand All @@ -30,12 +32,18 @@ public class Notification implements Serializable {
private String targetNickname;
private Action action;

//수락여부
private boolean isAccepted;

//읽기 여부
private boolean isRead;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
private String timestamp;

public static Notification of(NotificationRequest request){
return Notification.builder()
.NotificationId(UUID.randomUUID())
.notificationId(UUID.randomUUID())
.roomId(request.roomId())
.roomName(request.roomName())
.senderId(request.senderId())
Expand All @@ -46,4 +54,10 @@ public static Notification of(NotificationRequest request){
.timestamp(LocalDateTime.now().toString())
.build();
}

public void updateReply(boolean isAccepted) {
this.isAccepted = isAccepted;
this.isRead = true;
}

}
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package urdego.io.urdego_notification_service.domain.service;

import urdego.io.urdego_notification_service.controller.dto.request.notification.NotificationRequest;
import org.springframework.stereotype.Service;
import urdego.io.urdego_notification_service.controller.dto.WebSocketMessage;
import urdego.io.urdego_notification_service.controller.dto.request.ReplyRequest;
import urdego.io.urdego_notification_service.controller.dto.request.notification.NotificationRequest;
import urdego.io.urdego_notification_service.domain.entity.Notification;

import java.util.List;
import java.util.UUID;

public interface NotificationService {
//저장
void saveNotification(Notification notification);

//메세지 발행
public WebSocketMessage<Notification> publishNotification(NotificationRequest notificationRequest);

//사용자 별 메세지 확인
List<Object> getUserNotifications(Long userId);
// 답장 및 읽음 상태 변경
Notification updateReadStatus(ReplyRequest request, Long userId);

// 읽음 상태 저장
void updateReadStatus(Long userId, String lastReadMessageId);

//읽음 상태 조회
String getLastReadMessage(String userId);

void saveNotification(Notification notification);
//조회
List<Notification> readNotificationList(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import urdego.io.urdego_notification_service.common.enums.MessageType;
import urdego.io.urdego_notification_service.controller.dto.request.notification.NotificationRequest;
import urdego.io.urdego_notification_service.common.exception.notification.InvalidNotificationId;
import urdego.io.urdego_notification_service.common.exception.notification.NotFoundNotification;
import urdego.io.urdego_notification_service.controller.client.GameServiceClient;
import urdego.io.urdego_notification_service.controller.dto.WebSocketMessage;
import urdego.io.urdego_notification_service.controller.dto.request.ReplyRequest;
import urdego.io.urdego_notification_service.controller.dto.request.notification.NotificationRequest;
import urdego.io.urdego_notification_service.domain.entity.Notification;

import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@RequiredArgsConstructor
@Service
@Slf4j
public class NotificationServiceImpl implements NotificationService {
private final SimpMessagingTemplate simpMessagingTemplate;
private final RedisTemplate<String, Object> redisTemplate;
private final GameServiceClient gameServiceClient;
private static final String PREFIX = "urdego_notification:";
private static final long EXPIRATION_TIME = 3;
private static final long EXPIRATION_TIME = 2; //2일

@Override
public WebSocketMessage<Notification> publishNotification(NotificationRequest request) {
Notification notification = Notification.of(request);

//프로토콜 감싸기
WebSocketMessage<Notification> message = new WebSocketMessage<>(MessageType.NOTIFICATION, notification);
simpMessagingTemplate.convertAndSend("/urdego/sub/notifications/" + notification.getTargetId(), message);
Expand All @@ -37,18 +43,25 @@ public WebSocketMessage<Notification> publishNotification(NotificationRequest re
}

@Override
public List<Object> getUserNotifications(Long userId) {
return redisTemplate.opsForList().range(PREFIX + userId, 0, -1);
}
public Notification updateReadStatus(ReplyRequest request, Long userId) {
// 키 생성
String key = PREFIX + userId;

@Override
public void updateReadStatus(Long userId, String lastReadMessageId) {
List<Notification> notifications = readNotificationList(userId);

}
//notificationId의 알림 index 찾기 없으면 Exception!!
int index = IntStream.range(0, notifications.size())
.filter(i -> notifications.get(i).getNotificationId().toString().equals(request.notificationId()))
.findFirst().orElseThrow(() -> InvalidNotificationId.EXCEPTION);

@Override
public String getLastReadMessage(String userId) {
return "";
Notification updatedNotification = notifications.get(index);
updatedNotification.updateReply(request.isAccepted());
simpMessagingTemplate.convertAndSend("/urdego/sub/notifications/" + updatedNotification.getTargetId(), updatedNotification);

//redis에 수정사항 저장
//TODO 수정 후 redis에 저장이 안됨..;;
redisTemplate.opsForList().set(key,index, updatedNotification);
return updatedNotification;
}

@Override
Expand All @@ -57,4 +70,19 @@ public void saveNotification(Notification notification) {
redisTemplate.opsForList().rightPush(key, notification);
redisTemplate.expire(key,EXPIRATION_TIME, TimeUnit.DAYS);
}

@Override
public List<Notification> readNotificationList(Long userId) {
// 키 생성
String key = PREFIX + userId;

List<Object> rawNotification = redisTemplate.opsForList().range(key, 0, -1);
if(rawNotification == null || rawNotification.size() == 0) { throw NotFoundNotification.EXCEPTION;}

//Object -> Notification
List<Notification> notifications = rawNotification.stream().filter(obj -> obj instanceof Notification)
.map(obj -> (Notification) obj).collect(Collectors.toList());

return notifications;
}
}

0 comments on commit 2971b99

Please sign in to comment.