From d91c21c9b41567eac3f1f3cfa70fd98c6cf0be4f Mon Sep 17 00:00:00 2001 From: Donghoon Jeong <112836685+jjeongdong@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:18:48 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1,=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enroll/controller/EnrollController.java | 2 +- .../domain/enroll/service/EnrollService.java | 2 +- .../enroll/service/EnrollServiceImpl.java | 21 +++++- .../controller/NotificationController.java | 37 ++++++++++ .../converter/NotificationConverter.java | 51 ++++++++++++-- .../dto/NotificationResponse.java | 14 ++++ .../notification/entity/Notification.java | 3 + .../message/NotificationMessages.java | 3 + .../repository/NotificationRepository.java | 7 ++ .../service/NotificationService.java | 13 ++++ .../service/NotificationServiceImpl.java | 67 +++++++++++++++++++ .../domain/user/service/UserServiceImpl.java | 1 - 12 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/back/catchmate/domain/notification/controller/NotificationController.java create mode 100644 src/main/java/com/back/catchmate/domain/notification/service/NotificationService.java create mode 100644 src/main/java/com/back/catchmate/domain/notification/service/NotificationServiceImpl.java diff --git a/src/main/java/com/back/catchmate/domain/enroll/controller/EnrollController.java b/src/main/java/com/back/catchmate/domain/enroll/controller/EnrollController.java index bf9b6ff..643c371 100644 --- a/src/main/java/com/back/catchmate/domain/enroll/controller/EnrollController.java +++ b/src/main/java/com/back/catchmate/domain/enroll/controller/EnrollController.java @@ -40,7 +40,7 @@ public class EnrollController { @Operation(summary = "직관 신청 API", description = "직관 신청을 요청하는 API 입니다.") public CreateEnrollInfo requestEnroll(@Valid @RequestBody CreateEnrollRequest createEnrollRequest, @PathVariable Long boardId, - @JwtValidation Long userId) { + @JwtValidation Long userId) throws IOException { return enrollService.requestEnroll(createEnrollRequest, boardId, userId); } diff --git a/src/main/java/com/back/catchmate/domain/enroll/service/EnrollService.java b/src/main/java/com/back/catchmate/domain/enroll/service/EnrollService.java index 5ed07aa..07e9f96 100644 --- a/src/main/java/com/back/catchmate/domain/enroll/service/EnrollService.java +++ b/src/main/java/com/back/catchmate/domain/enroll/service/EnrollService.java @@ -13,7 +13,7 @@ import java.io.IOException; public interface EnrollService { - CreateEnrollInfo requestEnroll(CreateEnrollRequest request, Long boardId, Long userId); + CreateEnrollInfo requestEnroll(CreateEnrollRequest request, Long boardId, Long userId) throws IOException; CancelEnrollInfo cancelEnroll(Long enrollId, Long userId); diff --git a/src/main/java/com/back/catchmate/domain/enroll/service/EnrollServiceImpl.java b/src/main/java/com/back/catchmate/domain/enroll/service/EnrollServiceImpl.java index 684ed79..c1c626c 100644 --- a/src/main/java/com/back/catchmate/domain/enroll/service/EnrollServiceImpl.java +++ b/src/main/java/com/back/catchmate/domain/enroll/service/EnrollServiceImpl.java @@ -4,16 +4,18 @@ import com.back.catchmate.domain.board.repository.BoardRepository; import com.back.catchmate.domain.enroll.converter.EnrollConverter; import com.back.catchmate.domain.enroll.dto.EnrollRequest.CreateEnrollRequest; -import com.back.catchmate.domain.enroll.dto.EnrollResponse; import com.back.catchmate.domain.enroll.dto.EnrollResponse.CancelEnrollInfo; import com.back.catchmate.domain.enroll.dto.EnrollResponse.CreateEnrollInfo; +import com.back.catchmate.domain.enroll.dto.EnrollResponse.NewEnrollCountInfo; import com.back.catchmate.domain.enroll.dto.EnrollResponse.PagedEnrollReceiveInfo; import com.back.catchmate.domain.enroll.dto.EnrollResponse.PagedEnrollRequestInfo; import com.back.catchmate.domain.enroll.dto.EnrollResponse.UpdateEnrollInfo; import com.back.catchmate.domain.enroll.entity.AcceptStatus; import com.back.catchmate.domain.enroll.entity.Enroll; import com.back.catchmate.domain.enroll.repository.EnrollRepository; +import com.back.catchmate.domain.notification.message.NotificationMessages; import com.back.catchmate.domain.notification.service.FCMService; +import com.back.catchmate.domain.notification.service.NotificationService; import com.back.catchmate.domain.user.entity.User; import com.back.catchmate.domain.user.repository.UserRepository; import com.back.catchmate.global.error.ErrorCode; @@ -35,11 +37,12 @@ public class EnrollServiceImpl implements EnrollService { private final UserRepository userRepository; private final BoardRepository boardRepository; private final FCMService fcmService; + private final NotificationService notificationService; private final EnrollConverter enrollConverter; @Override @Transactional - public CreateEnrollInfo requestEnroll(CreateEnrollRequest request, Long boardId, Long userId) { + public CreateEnrollInfo requestEnroll(CreateEnrollRequest request, Long boardId, Long userId) throws IOException { User user = userRepository.findById(userId) .orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND)); @@ -57,6 +60,18 @@ public CreateEnrollInfo requestEnroll(CreateEnrollRequest request, Long boardId, Enroll enroll = enrollConverter.toEntity(request, user, board); enrollRepository.save(enroll); + + String title = NotificationMessages.ENROLLMENT_NOTIFICATION_TITLE; + String body = String.format(NotificationMessages.ENROLLMENT_NOTIFICATION_BODY, user.getNickName()); + + // 게시글 작성자의 아이디를 통해 FCM 토큰 확인 + User boardWriter = userRepository.findById(board.getUser().getId()) + .orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND)); + + fcmService.sendMessage(boardWriter.getFcmToken(), title, body, boardId); + + // 데이터베이스에 저장 + notificationService.createNotification(title, body, enroll.getUser().getProfileImageUrl(), boardId, boardWriter.getId()); return enrollConverter.toCreateEnrollInfo(enroll); } @@ -119,7 +134,7 @@ public PagedEnrollReceiveInfo getReceiveEnrollListByBoardId(Long userId, Long bo @Override @Transactional(readOnly = true) - public EnrollResponse.NewEnrollCountInfo getNewEnrollListCount(Long userId) { + public NewEnrollCountInfo getNewEnrollListCount(Long userId) { int enrollListCount = enrollRepository.countNewEnrollListByUserId(userId); return enrollConverter.toNewEnrollCountResponse(enrollListCount); } diff --git a/src/main/java/com/back/catchmate/domain/notification/controller/NotificationController.java b/src/main/java/com/back/catchmate/domain/notification/controller/NotificationController.java new file mode 100644 index 0000000..65737d4 --- /dev/null +++ b/src/main/java/com/back/catchmate/domain/notification/controller/NotificationController.java @@ -0,0 +1,37 @@ +package com.back.catchmate.domain.notification.controller; + +import com.back.catchmate.domain.notification.dto.NotificationResponse.NotificationInfo; +import com.back.catchmate.domain.notification.dto.NotificationResponse.PagedNotificationInfo; +import com.back.catchmate.domain.notification.service.NotificationService; +import com.back.catchmate.global.jwt.JwtValidation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "알림 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/notification") +public class NotificationController { + private final NotificationService notificationService; + + @GetMapping("/receive") + @Operation(summary = "내가 받은 알림 목록 조회 API", description = "내가 받은 알림 목록을 조회하는 API 입니다.") + public PagedNotificationInfo getNotificationList(@JwtValidation Long userId, + @Parameter(hidden = true) Pageable pageable) { + return notificationService.getNotificationList(userId, pageable); + } + + @GetMapping("/receive/{notificationId}") + @Operation(summary = "내가 받은 알림 단일 조회 API", description = "내가 받은 알림을 단일 조회하는 API 입니다.") + public NotificationInfo getNotification(@JwtValidation Long userId, + @PathVariable Long notificationId) { + return notificationService.getNotification(userId, notificationId); + } +} diff --git a/src/main/java/com/back/catchmate/domain/notification/converter/NotificationConverter.java b/src/main/java/com/back/catchmate/domain/notification/converter/NotificationConverter.java index 46a9cc9..d0dc0db 100644 --- a/src/main/java/com/back/catchmate/domain/notification/converter/NotificationConverter.java +++ b/src/main/java/com/back/catchmate/domain/notification/converter/NotificationConverter.java @@ -1,17 +1,58 @@ package com.back.catchmate.domain.notification.converter; -import com.back.catchmate.domain.notification.dto.NotificationResponse.CreateNotificationInfo; +import com.back.catchmate.domain.board.converter.BoardConverter; +import com.back.catchmate.domain.board.dto.BoardResponse.BoardInfo; +import com.back.catchmate.domain.board.entity.Board; +import com.back.catchmate.domain.notification.dto.NotificationResponse.NotificationInfo; +import com.back.catchmate.domain.notification.dto.NotificationResponse.PagedNotificationInfo; import com.back.catchmate.domain.notification.entity.Notification; +import com.back.catchmate.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; @Component +@RequiredArgsConstructor public class NotificationConverter { - public CreateNotificationInfo toCreateNotificationInfo(Notification notification) { - return CreateNotificationInfo.builder() + private final BoardConverter boardConverter; + + public Notification toEntity(User user, Board board, String senderProfileImageUrl, String title, String body) { + return Notification.builder() + .user(user) + .board(board) + .title(title) + .body(body) + .senderProfileImageUrl(senderProfileImageUrl) + .isRead(false) + .build(); + } + + public PagedNotificationInfo toPagedNotificationInfo(Page notificationList) { + List enrollRequestInfoList = notificationList.stream() + .map(notification -> toNotificationInfo(notification, notification.getBoard())) + .collect(Collectors.toList()); + + return PagedNotificationInfo.builder() + .notificationInfoList(enrollRequestInfoList) + .totalPages(notificationList.getTotalPages()) + .totalElements(notificationList.getTotalElements()) + .build(); + } + + public NotificationInfo toNotificationInfo(Notification notification, Board board) { + BoardInfo boardInfo = boardConverter.toBoardInfo(board); + + return NotificationInfo.builder() .notificationId(notification.getId()) - .createdAt(LocalDateTime.now()) + .title(notification.getTitle()) + .body(notification.getBody()) + .senderProfileImageUrl(notification.getSenderProfileImageUrl()) + .isRead(notification.isRead()) + .boardInfo(boardInfo) + .createdAt(notification.getCreatedAt()) .build(); } } diff --git a/src/main/java/com/back/catchmate/domain/notification/dto/NotificationResponse.java b/src/main/java/com/back/catchmate/domain/notification/dto/NotificationResponse.java index fe87201..7f7d354 100644 --- a/src/main/java/com/back/catchmate/domain/notification/dto/NotificationResponse.java +++ b/src/main/java/com/back/catchmate/domain/notification/dto/NotificationResponse.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.List; public abstract class NotificationResponse { @@ -17,6 +18,7 @@ public abstract class NotificationResponse { public static class NotificationInfo { private Long notificationId; private BoardInfo boardInfo; + private String senderProfileImageUrl; private String title; private String body; private LocalDateTime createdAt; @@ -31,4 +33,16 @@ public static class CreateNotificationInfo { private Long notificationId; private LocalDateTime createdAt; } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class PagedNotificationInfo { + private List notificationInfoList; + private Integer totalPages; + private Long totalElements; + private Boolean isFirst; + private Boolean isLast; + } } diff --git a/src/main/java/com/back/catchmate/domain/notification/entity/Notification.java b/src/main/java/com/back/catchmate/domain/notification/entity/Notification.java index 2726f58..299a9c7 100644 --- a/src/main/java/com/back/catchmate/domain/notification/entity/Notification.java +++ b/src/main/java/com/back/catchmate/domain/notification/entity/Notification.java @@ -38,6 +38,9 @@ public class Notification extends BaseTimeEntity { @JoinColumn(name = "board_id", nullable = false) private Board board; + @Column(nullable = false) + private String senderProfileImageUrl; + @Column(nullable = false) private String title; diff --git a/src/main/java/com/back/catchmate/domain/notification/message/NotificationMessages.java b/src/main/java/com/back/catchmate/domain/notification/message/NotificationMessages.java index 1970943..ad5e2f8 100644 --- a/src/main/java/com/back/catchmate/domain/notification/message/NotificationMessages.java +++ b/src/main/java/com/back/catchmate/domain/notification/message/NotificationMessages.java @@ -5,4 +5,7 @@ public class NotificationMessages { public static final String ENROLLMENT_REJECT_TITLE = "직관 신청 수락 안내 문자"; public static final String ENROLLMENT_ACCEPT_BODY = "귀하의 직관 신청이 승인되었습니다. 참여를 환영합니다!"; public static final String ENROLLMENT_REJECT_BODY = "귀하의 직관 신청이 거절되었습니다. 다음 기회에 참여를 부탁드립니다."; + + public static final String ENROLLMENT_NOTIFICATION_TITLE = "직관 신청 안내 문자"; + public static final String ENROLLMENT_NOTIFICATION_BODY = "%s님의 직관 신청이 도착했어요"; } diff --git a/src/main/java/com/back/catchmate/domain/notification/repository/NotificationRepository.java b/src/main/java/com/back/catchmate/domain/notification/repository/NotificationRepository.java index 4ef2ef7..a2c20ca 100644 --- a/src/main/java/com/back/catchmate/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/back/catchmate/domain/notification/repository/NotificationRepository.java @@ -1,7 +1,14 @@ package com.back.catchmate.domain.notification.repository; import com.back.catchmate.domain.notification.entity.Notification; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface NotificationRepository extends JpaRepository { + Page findByUserId(Long userId, Pageable pageable); + + Optional findByIdAndUserId(Long notificationId, Long userId); } diff --git a/src/main/java/com/back/catchmate/domain/notification/service/NotificationService.java b/src/main/java/com/back/catchmate/domain/notification/service/NotificationService.java new file mode 100644 index 0000000..fb72d36 --- /dev/null +++ b/src/main/java/com/back/catchmate/domain/notification/service/NotificationService.java @@ -0,0 +1,13 @@ +package com.back.catchmate.domain.notification.service; + +import com.back.catchmate.domain.notification.dto.NotificationResponse.NotificationInfo; +import com.back.catchmate.domain.notification.dto.NotificationResponse.PagedNotificationInfo; +import org.springframework.data.domain.Pageable; + +public interface NotificationService { + void createNotification(String title, String body, String senderProfileImageUrl, Long boardId, Long userId); + + PagedNotificationInfo getNotificationList(Long userId, Pageable pageable); + + NotificationInfo getNotification(Long userId, Long notificationId); +} diff --git a/src/main/java/com/back/catchmate/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/back/catchmate/domain/notification/service/NotificationServiceImpl.java new file mode 100644 index 0000000..23580ad --- /dev/null +++ b/src/main/java/com/back/catchmate/domain/notification/service/NotificationServiceImpl.java @@ -0,0 +1,67 @@ +package com.back.catchmate.domain.notification.service; + +import com.back.catchmate.domain.board.entity.Board; +import com.back.catchmate.domain.board.repository.BoardRepository; +import com.back.catchmate.domain.notification.converter.NotificationConverter; +import com.back.catchmate.domain.notification.dto.NotificationResponse.NotificationInfo; +import com.back.catchmate.domain.notification.dto.NotificationResponse.PagedNotificationInfo; +import com.back.catchmate.domain.notification.entity.Notification; +import com.back.catchmate.domain.notification.repository.NotificationRepository; +import com.back.catchmate.domain.user.entity.User; +import com.back.catchmate.domain.user.repository.UserRepository; +import com.back.catchmate.global.error.ErrorCode; +import com.back.catchmate.global.error.exception.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class NotificationServiceImpl implements NotificationService { + private final UserRepository userRepository; + private final BoardRepository boardRepository; + private final NotificationRepository notificationRepository; + private final NotificationConverter notificationConverter; + + @Override + @Transactional + public void createNotification(String title, String body, String senderProfileImageUrl, Long boardId, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND)); + + Board board = boardRepository.findById(boardId) + .orElseThrow(() -> new BaseException(ErrorCode.BOARD_NOT_FOUND)); + + Notification notification = notificationConverter.toEntity(user, board, senderProfileImageUrl, title, body); + notificationRepository.save(notification); + } + + @Override + @Transactional(readOnly = true) + public PagedNotificationInfo getNotificationList(Long userId, Pageable pageable) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND)); + + Page notificationList = notificationRepository.findByUserId(user.getId(), pageable); + return notificationConverter.toPagedNotificationInfo(notificationList); + } + + @Override + @Transactional + public NotificationInfo getNotification(Long userId, Long notificationId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND)); + + Notification notification = notificationRepository.findByIdAndUserId(notificationId, user.getId()) + .orElseThrow(() -> new BaseException(ErrorCode.NOTIFICATION_NOT_FOUND)); + + // 읽지 않은 알림일 경우, 읽음으로 표시 + if (notification.isNotRead()) { + notification.markAsRead(); + } + + return notificationConverter.toNotificationInfo(notification, notification.getBoard()); + } +} diff --git a/src/main/java/com/back/catchmate/domain/user/service/UserServiceImpl.java b/src/main/java/com/back/catchmate/domain/user/service/UserServiceImpl.java index 17e83a2..21c41ac 100644 --- a/src/main/java/com/back/catchmate/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/back/catchmate/domain/user/service/UserServiceImpl.java @@ -38,7 +38,6 @@ public class UserServiceImpl implements UserService{ private final ClubRepository clubRepository; private final RefreshTokenRepository refreshTokenRepository; private final UserConverter userConverter; - private final ClubConverter clubConverter; @Override @Transactional