Skip to content

Commit

Permalink
Merge pull request #154 from Team-Capple/feat/#150/saveNotifiaction
Browse files Browse the repository at this point in the history
[FEAT] 알림 리스트 저장, 조회, 삭제 기능 구현
  • Loading branch information
jaewonLeeKOR authored Sep 14, 2024
2 parents 42b1468 + df3ae32 commit 5c09631
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/reports", "/reports/**").authenticated()
.requestMatchers("/boards", "/boards/**").authenticated()
.requestMatchers("/boardComments", "/boardComments/**").authenticated()
.requestMatchers("/notifications", "/notifications/**").authenticated()
.requestMatchers("/dummy","/dummy/**").hasRole(Role.ROLE_ADMIN.getName())
.anyRequest().denyAll());
http
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.server.capple.domain.notifiaction.controller;

import com.server.capple.config.security.AuthMember;
import com.server.capple.domain.member.entity.Member;
import com.server.capple.domain.notifiaction.dto.NotificationResponse.NotificationInfo;
import com.server.capple.domain.notifiaction.service.NotificationService;
import com.server.capple.global.common.BaseResponse;
import com.server.capple.global.common.SliceResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "알림 API", description = "알림 API입니다.")
@RestController
@RequiredArgsConstructor
@RequestMapping("/notifications")
public class NotificationController {
private final NotificationService notificationService;

@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")))));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.server.capple.domain.notifiaction.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

import java.time.LocalDateTime;

public class NotificationResponse {
@Getter
@Builder
@ToString
@AllArgsConstructor
public static class NotificationInfo {
private String title;
private String subtitle;
private String content;
private String boardId;
private String boardCommentId;
private LocalDateTime createdAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.server.capple.domain.notifiaction.entity;

import com.server.capple.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Getter
@Entity
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.server.capple.domain.notifiaction.mapper;

import com.server.capple.config.apns.dto.ApnsClientRequest;
import com.server.capple.domain.notifiaction.entity.Notification;
import org.springframework.stereotype.Component;

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

public Notification toNotification(Long memberId, ApnsClientRequest.BoardCommentNotificationBody boardCommentNotificationBody) {
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())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.server.capple.domain.notifiaction.repository;

import com.server.capple.domain.notifiaction.entity.Notification;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;

public interface NotificationRepository extends JpaRepository<Notification, Long> {
<T> Slice<T> findByMemberId(Long memberId, Pageable pageable, Class<T> type);
void deleteNotificationsByCreatedAtBefore(LocalDateTime targetTime);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.server.capple.domain.notifiaction.scheduler;

import com.server.capple.domain.notifiaction.service.NotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Slf4j
@Component
@RequiredArgsConstructor
public class NotificationScheduler {
private final NotificationService notificationService;
private final long NOTIFICATION_CACHE_WEEK = 1L;

@Scheduled(cron = "0 0 * * * *") //매 0분에
public void deleteNotifications() {
LocalDateTime targetTime = LocalDateTime.now().minusWeeks(NOTIFICATION_CACHE_WEEK);
notificationService.deleteNotificationsByCreatedAtBefore(targetTime);
log.info("오래된 알림을 제거했습니다. 제거 기한 : " + targetTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@

import com.server.capple.domain.board.entity.Board;
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 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 deleteNotificationsByCreatedAtBefore(LocalDateTime targetTime);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package com.server.capple.domain.notifiaction.service;

import com.server.capple.config.apns.dto.ApnsClientRequest;
import com.server.capple.config.apns.dto.ApnsClientRequest.BoardCommentNotificationBody;
import com.server.capple.config.apns.dto.ApnsClientRequest.BoardNotificationBody;
import com.server.capple.config.apns.service.ApnsService;
import com.server.capple.domain.board.entity.Board;
import com.server.capple.domain.boardComment.entity.BoardComment;
import com.server.capple.domain.boardSubscribeMember.service.BoardSubscribeMemberService;
import com.server.capple.domain.member.entity.Member;
import com.server.capple.domain.notifiaction.dto.NotificationResponse.NotificationInfo;
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 jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

import static com.server.capple.domain.notifiaction.entity.NotificationType.*;
Expand All @@ -18,18 +27,24 @@
public class NotificationServiceImpl implements NotificationService {
private final ApnsService apnsService;
private final BoardSubscribeMemberService boardSubscribeMemberService;
private final NotificationRepository notificationRepository;
private final NotificationMapper notificationMapper;

@Override
public void sendBoardHeartNotification(Long actorId, Board board) {
if (actorId.equals(board.getWriter().getId())) return;
apnsService.sendApnsToMembers(ApnsClientRequest.BoardNotificationBody.builder()
BoardNotificationBody boardNotificationBody = BoardNotificationBody.builder()
.type(BOARD_HEART)
.board(board)
.build(), board.getWriter().getId());
.build();
apnsService.sendApnsToMembers(boardNotificationBody, board.getWriter().getId());
// TODO 알림 데이터베이스 저장
Notification notification = notificationMapper.toNotification(board.getWriter().getId(), boardNotificationBody);
notificationRepository.save(notification);
}

@Override
@Transactional
public void sendBoardCommentNotification(Long actorId, Board board, BoardComment boardComment) {
List<Member> subscribers = boardSubscribeMemberService.findBoardSubscribeMembers(board.getId());
List<Long> subscriberIds = subscribers.stream()
Expand All @@ -38,30 +53,51 @@ public void sendBoardCommentNotification(Long actorId, Board board, BoardComment
// 게시판 구독자에게 알림 전송
.peek(subscriberId -> {
if (subscriberId.equals(board.getWriter().getId())) {
apnsService.sendApnsToMembers(ApnsClientRequest.BoardCommentNotificationBody.builder()
BoardCommentNotificationBody boardCommentNotificationBody = BoardCommentNotificationBody.builder()
.type(BOARD_COMMENT)
.board(board)
.boardComment(boardComment)
.build(), subscriberId);
.build();
apnsService.sendApnsToMembers(boardCommentNotificationBody, subscriberId);
notificationRepository.save(notificationMapper.toNotification(subscriberId, boardCommentNotificationBody));
}
})
.filter(id -> !id.equals(board.getWriter().getId()))
.toList();
apnsService.sendApnsToMembers(ApnsClientRequest.BoardCommentNotificationBody.builder()
BoardCommentNotificationBody boardCommentNotificationBody = BoardCommentNotificationBody.builder()
.type(BAORD_COMMENT_DUPLCATE)
.board(board)
.boardComment(boardComment)
.build(), subscriberIds);
.build();
apnsService.sendApnsToMembers(boardCommentNotificationBody, subscriberIds);
// TODO 알림 데이터베이스 저장
List<Notification> notifications = subscriberIds.stream()
.map(subscriberId -> notificationMapper.toNotification(subscriberId, boardCommentNotificationBody))
.toList();
notificationRepository.saveAll(notifications);
}

@Override
public void sendBoardCommentHeartNotification(Long actorId, Board board, BoardComment boardComment) {
apnsService.sendApnsToMembers(ApnsClientRequest.BoardCommentNotificationBody.builder()
BoardCommentNotificationBody boardCommentNotificationBody = BoardCommentNotificationBody.builder()
.type(BOARD_COMMENT_HEART)
.board(board)
.boardComment(boardComment)
.build(), boardComment.getMember().getId());
.build();
apnsService.sendApnsToMembers(boardCommentNotificationBody, boardComment.getMember().getId());
// TODO 알림 데이터베이스 저장
Notification notification = notificationMapper.toNotification(boardComment.getMember().getId(), boardCommentNotificationBody);
notificationRepository.save(notification);
}

@Override
public Slice<NotificationInfo> getNotifications(Member member, Pageable pageable) {
return notificationRepository.findByMemberId(member.getId(), pageable, NotificationInfo.class);
}

@Override
@Transactional
public void deleteNotificationsByCreatedAtBefore(LocalDateTime targetTime) {
notificationRepository.deleteNotificationsByCreatedAtBefore(targetTime);
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/server/capple/global/common/SliceResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.server.capple.global.common;

import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.domain.Slice;

import java.util.List;

@Getter
@NoArgsConstructor
public class SliceResponse<T> {
int number;
int size;
List<T> content;
int numberOfElements;
boolean hasPrevious;
boolean hasNext;
public SliceResponse(Slice<T> sliceObject) {
number = sliceObject.getNumber();
size = sliceObject.getSize();
content = sliceObject.getContent();
numberOfElements = sliceObject.getNumberOfElements();
hasPrevious = sliceObject.hasPrevious();
hasNext = sliceObject.hasNext();
}
}

0 comments on commit 5c09631

Please sign in to comment.