Skip to content

Commit

Permalink
feat: #173 질문 생성/종료 원격 알림 구현 및 알림 엔티티 구성방식 변경
Browse files Browse the repository at this point in the history
  • Loading branch information
jaewonLeeKOR committed Sep 21, 2024
1 parent 519bfaa commit 8158f70
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.server.capple.domain.board.entity.Board;
import com.server.capple.domain.boardComment.entity.BoardComment;
import com.server.capple.domain.notifiaction.entity.NotificationType;
import com.server.capple.domain.question.entity.Question;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

Expand Down Expand Up @@ -134,4 +135,48 @@ public static class Alert {
}
}
}

@Getter
@NoArgsConstructor
@ToString
public static class QuestionNotificationBody {
private Aps aps;
private String questionId;
@Builder
public QuestionNotificationBody(NotificationType type, Question question) {
this.aps = Aps.builder().threadId("question")
.alert(Aps.Alert.builder()
.title(type.getTitle())
.subtitle(type.getContent().replace("\n", " "))
.body(question.getContent())
.build())
.build();
this.questionId = question.getId().toString();
}

@ToString
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Aps {
private Alert alert;
private Integer badge;
@Schema(defaultValue = "default")
private String sound; // Library/Sounds 폴더 내의 파일 이름
@JsonProperty("thread-id")
private String threadId;

@ToString
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Alert {
private String title;
private String subtitle;
private String body;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public interface ApnsService {
<T> Boolean sendApns(T request, List<String> deviceTokenList);
<T> Boolean sendApnsToMembers(T request, Long ... memberIds);
<T> Boolean sendApnsToMembers(T request, List<Long> memberIdList);
<T> Boolean sendApnsToAllMembers(T request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,9 @@ public <T> Boolean sendApnsToMembers(T request, Long... memberIds) {
public <T> Boolean sendApnsToMembers(T request, List<Long> memberIdList) {
return sendApns(request, deviceTokenRedisRepository.getDeviceTokens(memberIdList));
}

@Override
public <T> Boolean sendApnsToAllMembers(T request) {
return sendApns(request, deviceTokenRedisRepository.getAllDeviceTokens());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public List<String> getDeviceTokens(List<Long> keys) {
return valueOperations.multiGet(keys.stream().map(key -> DEVICE_TOKEN_KEY + key.toString()).toList());
}

public List<String> getAllDeviceTokens() {
return valueOperations.multiGet(redisTemplate.keys(DEVICE_TOKEN_KEY + "*"));
}

public void deleteDeviceToken(Long memberId) {
redisTemplate.delete(DEVICE_TOKEN_KEY + memberId.toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public class NotificationController {
@Operation(summary = "알림 리스트 조회 API", description = "API를 호출한 사용자가 받은 알림 리스트를 조회합니다. 알림은 최신순으로 정렬되어 반환됩니다.")
@GetMapping
public BaseResponse<SliceResponse<NotificationInfo>> getNotifications(@AuthMember Member member, @RequestParam(defaultValue = "0", required = false) Integer pageNumber, @RequestParam(defaultValue = "1000", required = false) Integer pageSize) {
return BaseResponse.onSuccess(new SliceResponse<>(notificationService.getNotifications(member, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt")))));
return BaseResponse.onSuccess(notificationService.getNotifications(member, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static class NotificationInfo {
private String subtitle;
private String content;
private String boardId;
private String questionId;
private String boardCommentId;
private LocalDateTime createdAt;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.server.capple.domain.notifiaction.entity;

import com.server.capple.domain.board.entity.Board;
import com.server.capple.domain.boardComment.entity.BoardComment;
import com.server.capple.domain.question.entity.Question;
import com.server.capple.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
Expand All @@ -13,14 +16,13 @@ public class Notification extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long memberId;
@Column(nullable = false)
private String title;
private String subtitle;
@Column(nullable = false)
private String content;
@Column(nullable = false)
private String boardId;
private String boardCommentId;
@ManyToOne(fetch = FetchType.LAZY)
private Board board;
@ManyToOne(fetch = FetchType.LAZY)
private BoardComment boardComment;
@ManyToOne(fetch = FetchType.LAZY)
private Question question;
@Enumerated(EnumType.ORDINAL)
private NotificationType type;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
public enum NotificationType {
BOARD_HEART("누군가 내 게시글을 좋아했어요", null),
BOARD_COMMENT("누군가 내 게시글에 댓글을 달았어요", null),
BAORD_COMMENT_DUPLCATE("누군가 나와 같은 게시글에 댓글을 달았아요", null),
BOARD_COMMENT_DUPLICATE("누군가 같은 게시글에 댓글을 달았어요", null),
BOARD_COMMENT_HEART("누군가 내 댓글을 좋아했어요", null),
TODAY_QUESTION_PUBLISHED("오늘의 질문", "오늘의 질문이 준비 되었어요.\n지금 바로 답변해보세요."),
TODAY_QUESTION_CLOSED("오늘의 질문", "오늘의 질문 답변 시간이 마감되었어요.\n다른 러너들은 어떻게 답 했는지 확인해보세요."),
TODAY_QUESTION_PUBLISHED("오늘의 질문 준비 완료!", "오늘의 질문이 준비 되었어요.\n지금 바로 답변해보세요."),
TODAY_QUESTION_CLOSED("오늘의 질문 마감 알림", "오늘의 질문 답변 시간이 마감되었어요.\n다른 러너들은 어떻게 답 했는지 확인해보세요."),
;
private final String title;
private final String content;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,83 @@
package com.server.capple.domain.notifiaction.mapper;

import com.server.capple.config.apns.dto.ApnsClientRequest;
import com.server.capple.domain.board.entity.Board;
import com.server.capple.domain.boardComment.entity.BoardComment;
import com.server.capple.domain.notifiaction.dto.NotificationResponse.NotificationInfo;
import com.server.capple.domain.notifiaction.entity.Notification;
import com.server.capple.domain.notifiaction.entity.NotificationType;
import com.server.capple.domain.question.entity.Question;
import com.server.capple.global.common.SliceResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class NotificationMapper {
public Notification toNotification(Long memberId, ApnsClientRequest.BoardNotificationBody boardNotificationBody) {
public Notification toNotification(Long memberId, Board board, NotificationType type) {
return Notification.builder()
.memberId(memberId)
.title(boardNotificationBody.getAps().getAlert().getTitle())
.content(boardNotificationBody.getAps().getAlert().getBody())
.boardId(boardNotificationBody.getBoardId())
.board(board)
.type(type)
.build();
}

public Notification toNotification(Long memberId, ApnsClientRequest.BoardCommentNotificationBody boardCommentNotificationBody) {
public Notification toNotification(Long memberId, Board board, BoardComment boardComment, NotificationType type) {
return Notification.builder()
.memberId(memberId)
.title(boardCommentNotificationBody.getAps().getAlert().getTitle())
.subtitle(boardCommentNotificationBody.getAps().getAlert().getSubtitle())
.content(boardCommentNotificationBody.getAps().getAlert().getBody())
.boardId(boardCommentNotificationBody.getBoardId())
.boardCommentId(boardCommentNotificationBody.getBoardCommentId())
.board(board)
.boardComment(boardComment)
.type(type)
.build();
}

public Notification toNotification(Question question, NotificationType type) {
return Notification.builder()
.question(question)
.type(type)
.build();
}

public SliceResponse<NotificationInfo> toNotificationInfoSlice(Slice<Notification> notification) {
return SliceResponse.toSliceResponse(notification, notification.stream().map(this::toNotificationInfo).toList());
}

private NotificationInfo toNotificationInfo(Notification notification) {
return switch (notification.getType()) {
case BOARD_HEART -> toBoardNotificationInfo(notification);
case BOARD_COMMENT, BOARD_COMMENT_DUPLICATE, BOARD_COMMENT_HEART ->
toBoardCommentNotificationInfo(notification);
case TODAY_QUESTION_PUBLISHED, TODAY_QUESTION_CLOSED -> toQuestionNotificationInfo(notification);
};
}

private NotificationInfo toBoardNotificationInfo(Notification notification) {
return NotificationInfo.builder()
.title(notification.getType().getTitle())
.content(notification.getBoard().getContent())
.boardId(notification.getBoard().getId().toString())
.createdAt(notification.getCreatedAt())
.build();
}

private NotificationInfo toBoardCommentNotificationInfo(Notification notification) {
return NotificationInfo.builder()
.title(notification.getType().getTitle())
.subtitle(notification.getBoardComment().getContent())
.content(notification.getBoard().getContent())
.boardId(notification.getBoard().getId().toString())
.boardCommentId(notification.getBoardComment().getId().toString())
.createdAt(notification.getCreatedAt())
.build();
}

private NotificationInfo toQuestionNotificationInfo(Notification notification) {
return NotificationInfo.builder()
.title(notification.getType().getTitle())
.subtitle(notification.getType().getContent())
.content(notification.getQuestion().getContent())
.questionId(notification.getQuestion().getId().toString())
.createdAt(notification.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.time.LocalDateTime;

public interface NotificationRepository extends JpaRepository<Notification, Long> {
<T> Slice<T> findByMemberId(Long memberId, Pageable pageable, Class<T> type);
@Query("select " +
"n " +
"from Notification n " +
"where " +
"n.memberId = :memberId " +
"OR " +
"n.type = com.server.capple.domain.notifiaction.entity.NotificationType.TODAY_QUESTION_PUBLISHED " +
"OR " +
"n.type = com.server.capple.domain.notifiaction.entity.NotificationType.TODAY_QUESTION_CLOSED"
)
Slice<Notification> findByMemberId(Long memberId, Pageable pageable);
void deleteNotificationsByCreatedAtBefore(LocalDateTime targetTime);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class NotificationScheduler {
private final NotificationService notificationService;
private final long NOTIFICATION_CACHE_WEEK = 1L;

@Scheduled(cron = "0 0 * * * *") //매 0분에
@Scheduled(cron = "0 0 * * * *") // 정각마다
public void deleteNotifications() {
LocalDateTime targetTime = LocalDateTime.now().minusWeeks(NOTIFICATION_CACHE_WEEK);
notificationService.deleteNotificationsByCreatedAtBefore(targetTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
import com.server.capple.domain.boardComment.entity.BoardComment;
import com.server.capple.domain.member.entity.Member;
import com.server.capple.domain.notifiaction.dto.NotificationResponse;
import com.server.capple.domain.question.entity.Question;
import com.server.capple.global.common.SliceResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

import java.time.LocalDateTime;

public interface NotificationService {
void sendBoardHeartNotification(Long actorId, Board board);
void sendBoardCommentNotification(Long actorId, Board board, BoardComment boardComment);
void sendBoardCommentHeartNotification(Long actorId, Board board, BoardComment boardComment);
Slice<NotificationResponse.NotificationInfo> getNotifications(Member member, Pageable pageable);
void sendLiveQuestionOpenNotification(Question question);
void sendLiveQuestionCloseNotification(Question question);
SliceResponse<NotificationResponse.NotificationInfo> getNotifications(Member member, Pageable pageable);
void deleteNotificationsByCreatedAtBefore(LocalDateTime targetTime);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.server.capple.config.apns.dto.ApnsClientRequest.BoardCommentNotificationBody;
import com.server.capple.config.apns.dto.ApnsClientRequest.BoardNotificationBody;
import com.server.capple.config.apns.dto.ApnsClientRequest.QuestionNotificationBody;
import com.server.capple.config.apns.service.ApnsService;
import com.server.capple.domain.board.entity.Board;
import com.server.capple.domain.boardComment.entity.BoardComment;
Expand All @@ -11,6 +12,8 @@
import com.server.capple.domain.notifiaction.entity.Notification;
import com.server.capple.domain.notifiaction.mapper.NotificationMapper;
import com.server.capple.domain.notifiaction.repository.NotificationRepository;
import com.server.capple.domain.question.entity.Question;
import com.server.capple.global.common.SliceResponse;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -39,7 +42,7 @@ public void sendBoardHeartNotification(Long actorId, Board board) {
.build();
apnsService.sendApnsToMembers(boardNotificationBody, board.getWriter().getId());
// 알림 데이터베이스 저장
Notification notification = notificationMapper.toNotification(board.getWriter().getId(), boardNotificationBody);
Notification notification = notificationMapper.toNotification(board.getWriter().getId(), board, BOARD_HEART);
notificationRepository.save(notification);
}

Expand All @@ -59,20 +62,20 @@ public void sendBoardCommentNotification(Long actorId, Board board, BoardComment
.boardComment(boardComment)
.build();
apnsService.sendApnsToMembers(boardCommentNotificationBody, subscriberId);
notificationRepository.save(notificationMapper.toNotification(subscriberId, boardCommentNotificationBody));
notificationRepository.save(notificationMapper.toNotification(subscriberId, board, boardComment, BOARD_COMMENT));
}
})
.filter(id -> !id.equals(board.getWriter().getId()))
.toList();
BoardCommentNotificationBody boardCommentNotificationBody = BoardCommentNotificationBody.builder()
.type(BAORD_COMMENT_DUPLCATE)
.type(BOARD_COMMENT_DUPLICATE)
.board(board)
.boardComment(boardComment)
.build();
apnsService.sendApnsToMembers(boardCommentNotificationBody, subscriberIds);
// 알림 데이터베이스 저장
List<Notification> notifications = subscriberIds.stream()
.map(subscriberId -> notificationMapper.toNotification(subscriberId, boardCommentNotificationBody))
.map(subscriberId -> notificationMapper.toNotification(subscriberId, board, boardComment, BOARD_COMMENT_DUPLICATE))
.toList();
notificationRepository.saveAll(notifications);
}
Expand All @@ -86,13 +89,36 @@ public void sendBoardCommentHeartNotification(Long actorId, Board board, BoardCo
.build();
apnsService.sendApnsToMembers(boardCommentNotificationBody, boardComment.getWriter().getId());
// 알림 데이터베이스 저장
Notification notification = notificationMapper.toNotification(boardComment.getWriter().getId(), boardCommentNotificationBody);
Notification notification = notificationMapper.toNotification(boardComment.getWriter().getId(), board, boardComment, BOARD_COMMENT_HEART);
notificationRepository.save(notification);
}

@Override
public Slice<NotificationInfo> getNotifications(Member member, Pageable pageable) {
return notificationRepository.findByMemberId(member.getId(), pageable, NotificationInfo.class);
public void sendLiveQuestionOpenNotification(Question question) {
QuestionNotificationBody questionNotificationBody = QuestionNotificationBody.builder()
.type(TODAY_QUESTION_PUBLISHED)
.question(question)
.build();
apnsService.sendApnsToAllMembers(questionNotificationBody);
Notification notification = notificationMapper.toNotification(question, TODAY_QUESTION_PUBLISHED);
notificationRepository.save(notification);
}

@Override
public void sendLiveQuestionCloseNotification(Question question) {
QuestionNotificationBody questionNotificationBody = QuestionNotificationBody.builder()
.type(TODAY_QUESTION_CLOSED)
.question(question)
.build();
apnsService.sendApnsToAllMembers(questionNotificationBody);
Notification notification = notificationMapper.toNotification(question, TODAY_QUESTION_CLOSED);
notificationRepository.save(notification);
}

@Override
public SliceResponse<NotificationInfo> getNotifications(Member member, Pageable pageable) {
Slice<Notification> notificationRDBInfos = notificationRepository.findByMemberId(member.getId(), pageable);
return notificationMapper.toNotificationInfoSlice(notificationRDBInfos);
}

@Override
Expand Down
Loading

0 comments on commit 8158f70

Please sign in to comment.