Skip to content

Commit

Permalink
Merge pull request #174 from Team-Capple/feat/#173/questionNotification
Browse files Browse the repository at this point in the history
[FEAT] 질문 생성/종료 원격 알림 구현 및 알림 엔티티 구성방식 변경
  • Loading branch information
jaewonLeeKOR authored Sep 24, 2024
2 parents 280d47d + 6d0ce45 commit edf5e35
Show file tree
Hide file tree
Showing 18 changed files with 259 additions and 53 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,47 @@ 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())
.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 @@ -58,7 +58,7 @@ public <T> Boolean sendApns(T request, List<String> deviceToken) {

deviceToken.parallelStream()
.forEach(token -> {
if (token.isBlank()) return;
if (token == null || token.isBlank() || token.equals("string")) return;
tmpWebClient
.method(HttpMethod.POST)
.uri(token)
Expand Down 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
Expand Up @@ -13,14 +13,9 @@ 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, cascade = CascadeType.ALL)
private NotificationLog notificationLog;
@Enumerated(EnumType.ORDINAL)
private NotificationType type;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.server.capple.domain.notifiaction.entity;

import com.server.capple.global.common.BaseEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

@Getter
@Entity
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class NotificationLog extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String subtitle;
private String body;
private Long boardId;
private Long boardCommentId;
private Long questionId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
@Getter
@RequiredArgsConstructor
public enum NotificationType {
BOARD_HEART("누군가 내 게시글을 좋아했어요", null),
BOARD_COMMENT("누군가 내 게시글에 댓글을 달았어요", null),
BAORD_COMMENT_DUPLCATE("누군가 나와 같은 게시글에 댓글을 달았아요", null),
BOARD_COMMENT_HEART("누군가 내 댓글을 좋아했어요", null),
TODAY_QUESTION_PUBLISHED("오늘의 질문", "오늘의 질문이 준비 되었어요.\n지금 바로 답변해보세요."),
TODAY_QUESTION_CLOSED("오늘의 질문", "오늘의 질문 답변 시간이 마감되었어요.\n다른 러너들은 어떻게 답 했는지 확인해보세요."),
BOARD_HEART("누군가 내 게시글을 좋아했어요"),
BOARD_COMMENT("누군가 내 게시글에 댓글을 달았어요"),
BOARD_COMMENT_DUPLICATE("누군가 같은 게시글에 댓글을 달았어요"),
BOARD_COMMENT_HEART("누군가 내 댓글을 좋아했어요"),
TODAY_QUESTION_PUBLISHED("오늘의 질문 준비 완료!"),
TODAY_QUESTION_CLOSED("오늘의 질문 답변 마감!"),
;
private final String title;
private final String content;
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,97 @@
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.NotificationLog;
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, NotificationLog notificationLog, NotificationType type) {
return Notification.builder()
.memberId(memberId)
.title(boardNotificationBody.getAps().getAlert().getTitle())
.content(boardNotificationBody.getAps().getAlert().getBody())
.boardId(boardNotificationBody.getBoardId())
.notificationLog(notificationLog)
.type(type)
.build();
}

public Notification toNotification(Long memberId, ApnsClientRequest.BoardCommentNotificationBody boardCommentNotificationBody) {
public Notification toNotification(NotificationLog notificationLog, 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())
.notificationLog(notificationLog)
.type(type)
.build();
}

public NotificationLog toNotificationLog(Board board) {
return NotificationLog.builder()
.body(board.getContent())
.boardId(board.getId())
.build();
}

public NotificationLog toNotificationLog(Board board, BoardComment boardComment) {
return NotificationLog.builder()
.subtitle(boardComment.getContent())
.body(board.getContent())
.boardId(board.getId())
.boardCommentId(boardComment.getId())
.build();
}

public NotificationLog toNotificationLog(Question question) {
return NotificationLog.builder()
.body(question.getContent())
.questionId(question.getId())
.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.getNotificationLog().getBody())
.boardId(notification.getNotificationLog().getBoardId().toString())
.createdAt(notification.getCreatedAt())
.build();
}

private NotificationInfo toBoardCommentNotificationInfo(Notification notification) {
return NotificationInfo.builder()
.title(notification.getType().getTitle())
.subtitle(notification.getNotificationLog().getSubtitle())
.content(notification.getNotificationLog().getBody())
.boardId(notification.getNotificationLog().getBoardId().toString())
.boardCommentId(notification.getNotificationLog().getBoardCommentId().toString())
.createdAt(notification.getCreatedAt())
.build();
}

private NotificationInfo toQuestionNotificationInfo(Notification notification) {
return NotificationInfo.builder()
.title(notification.getType().getTitle())
.content(notification.getNotificationLog().getBody())
.questionId(notification.getNotificationLog().getQuestionId().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);
}
Loading

0 comments on commit edf5e35

Please sign in to comment.