diff --git a/src/main/java/com/server/capple/CappleApplication.java b/src/main/java/com/server/capple/CappleApplication.java index bcd0e14b..a032fc21 100644 --- a/src/main/java/com/server/capple/CappleApplication.java +++ b/src/main/java/com/server/capple/CappleApplication.java @@ -7,6 +7,7 @@ import org.springframework.cache.annotation.EnableCaching; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import java.time.ZoneId; @@ -18,6 +19,7 @@ @EnableConfigurationProperties @EnableScheduling @EnableCaching +@EnableAsync public class CappleApplication { public static void main(String[] args) { diff --git a/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java b/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java index 86da0670..3e1a16e8 100644 --- a/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java +++ b/src/main/java/com/server/capple/config/apns/dto/ApnsClientRequest.java @@ -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.*; @@ -101,8 +102,7 @@ public BoardCommentNotificationBody(NotificationType type, Board board, BoardCom this.aps = Aps.builder().threadId("board-" + board.getId()) .alert(Aps.Alert.builder() .title(type.getTitle()) - .subtitle(boardComment.getContent()) - .body(board.getContent()) + .body(boardComment.getContent()) .build()) .build(); this.boardId = board.getId().toString(); @@ -134,4 +134,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; + } + } + } } diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsService.java b/src/main/java/com/server/capple/config/apns/service/ApnsService.java index 05032e3c..1e1e8cef 100644 --- a/src/main/java/com/server/capple/config/apns/service/ApnsService.java +++ b/src/main/java/com/server/capple/config/apns/service/ApnsService.java @@ -7,4 +7,5 @@ public interface ApnsService { Boolean sendApns(T request, List deviceTokenList); Boolean sendApnsToMembers(T request, Long ... memberIds); Boolean sendApnsToMembers(T request, List memberIdList); + Boolean sendApnsToAllMembers(T request); } diff --git a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java index 592c102a..fb540b53 100644 --- a/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java +++ b/src/main/java/com/server/capple/config/apns/service/ApnsServiceImpl.java @@ -58,7 +58,7 @@ public Boolean sendApns(T request, List deviceToken) { deviceToken.parallelStream() .forEach(token -> { - if (token.isBlank()) return; + if (token == null || token.isBlank() || token.equals("string")) return; tmpWebClient .method(HttpMethod.POST) .uri(token) @@ -99,4 +99,9 @@ public Boolean sendApnsToMembers(T request, Long... memberIds) { public Boolean sendApnsToMembers(T request, List memberIdList) { return sendApns(request, deviceTokenRedisRepository.getDeviceTokens(memberIdList)); } + + @Override + public Boolean sendApnsToAllMembers(T request) { + return sendApns(request, deviceTokenRedisRepository.getAllDeviceTokens()); + } } diff --git a/src/main/java/com/server/capple/domain/mail/service/MailServiceImpl.java b/src/main/java/com/server/capple/domain/mail/service/MailServiceImpl.java index 08b36210..8ac8df0f 100644 --- a/src/main/java/com/server/capple/domain/mail/service/MailServiceImpl.java +++ b/src/main/java/com/server/capple/domain/mail/service/MailServiceImpl.java @@ -7,8 +7,10 @@ import com.server.capple.global.exception.RestApiException; import com.server.capple.global.exception.errorCode.MailErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public class MailServiceImpl implements MailService { @@ -19,9 +21,12 @@ public class MailServiceImpl implements MailService { @Override public Boolean sendMailAddressCertificationMail(String email, Boolean isWhiteList) { - String certCode = mailUtil.sendMailAddressCertificationMail(email, isWhiteList); - String emailJwt = jwtService.createJwtFromEmail(email); - return mailRedisRepository.save(emailJwt, certCode); + mailUtil.sendMailAddressCertificationMail(email, isWhiteList).thenAccept(certCode -> { + String emailJwt = jwtService.createJwtFromEmail(email); + mailRedisRepository.save(emailJwt, certCode); + log.info("메일 발송 완료 : {}", email); + }); + return true; } @Override diff --git a/src/main/java/com/server/capple/domain/mail/service/MailUtil.java b/src/main/java/com/server/capple/domain/mail/service/MailUtil.java index 538c7fa2..efe6018d 100644 --- a/src/main/java/com/server/capple/domain/mail/service/MailUtil.java +++ b/src/main/java/com/server/capple/domain/mail/service/MailUtil.java @@ -1,10 +1,12 @@ package com.server.capple.domain.mail.service; +import java.util.concurrent.CompletableFuture; + public interface MailUtil { public static Boolean emailAddressFormVerification(String emailAddress) { String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$"; return emailAddress.matches(emailRegex); } - String sendMailAddressCertificationMail(String receiver, Boolean isWhiteList); + CompletableFuture sendMailAddressCertificationMail(String receiver, Boolean isWhiteList); } diff --git a/src/main/java/com/server/capple/domain/mail/service/MailUtilImpl.java b/src/main/java/com/server/capple/domain/mail/service/MailUtilImpl.java index 60e91b0a..72037a52 100644 --- a/src/main/java/com/server/capple/domain/mail/service/MailUtilImpl.java +++ b/src/main/java/com/server/capple/domain/mail/service/MailUtilImpl.java @@ -5,13 +5,18 @@ import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; +import java.util.concurrent.CompletableFuture; + +@Slf4j @Component @RequiredArgsConstructor public class MailUtilImpl implements MailUtil { @@ -20,10 +25,11 @@ public class MailUtilImpl implements MailUtil { @Value("${mail.white-list-cert-code}") private String whiteListCertCode; + @Async @Override - public String sendMailAddressCertificationMail(String receiver, Boolean isWhiteList) { + public CompletableFuture sendMailAddressCertificationMail(String receiver, Boolean isWhiteList) { String certCode = generateCertCode(); - if(isWhiteList) certCode = whiteListCertCode; + if (isWhiteList) certCode = whiteListCertCode; MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); @@ -31,8 +37,9 @@ public String sendMailAddressCertificationMail(String receiver, Boolean isWhiteL mimeMessageHelper.setSubject("[Capple] 회원가입 인증코드 안내"); mimeMessageHelper.setText(setCertMailContext(certCode), true); javaMailSender.send(mimeMessage); - return certCode; + return CompletableFuture.completedFuture(certCode); } catch (MessagingException e) { + log.error(MailErrorCode.MULTI_PART_CRAETION_FAILED.getMessage()); throw new RestApiException(MailErrorCode.MULTI_PART_CRAETION_FAILED); } } @@ -42,7 +49,7 @@ private String generateCertCode() { final Integer certCodeLength = 5; String certCode = ""; for (int i = 0; i < certCodeLength; i++) { - Long idx = Math.round(Math.random() * candidateChars.length()); + Long idx = (long) (Math.random() * candidateChars.length()); certCode += candidateChars.charAt(idx.intValue()); } return certCode; diff --git a/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java b/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java index 994218ef..b7b5ca5d 100644 --- a/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java +++ b/src/main/java/com/server/capple/domain/member/repository/DeviceTokenRedisRepository.java @@ -30,6 +30,10 @@ public List getDeviceTokens(List keys) { return valueOperations.multiGet(keys.stream().map(key -> DEVICE_TOKEN_KEY + key.toString()).toList()); } + public List getAllDeviceTokens() { + return valueOperations.multiGet(redisTemplate.keys(DEVICE_TOKEN_KEY + "*")); + } + public void deleteDeviceToken(Long memberId) { redisTemplate.delete(DEVICE_TOKEN_KEY + memberId.toString()); } diff --git a/src/main/java/com/server/capple/domain/notifiaction/controller/NotificationController.java b/src/main/java/com/server/capple/domain/notifiaction/controller/NotificationController.java index c44c4a8c..d29ed52f 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/controller/NotificationController.java +++ b/src/main/java/com/server/capple/domain/notifiaction/controller/NotificationController.java @@ -26,6 +26,6 @@ public class NotificationController { @Operation(summary = "알림 리스트 조회 API", description = "API를 호출한 사용자가 받은 알림 리스트를 조회합니다. 알림은 최신순으로 정렬되어 반환됩니다.") @GetMapping public BaseResponse> 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")))); } } diff --git a/src/main/java/com/server/capple/domain/notifiaction/dto/NotificationResponse.java b/src/main/java/com/server/capple/domain/notifiaction/dto/NotificationResponse.java index 37aa2c54..b37571d4 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/dto/NotificationResponse.java +++ b/src/main/java/com/server/capple/domain/notifiaction/dto/NotificationResponse.java @@ -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; } diff --git a/src/main/java/com/server/capple/domain/notifiaction/entity/Notification.java b/src/main/java/com/server/capple/domain/notifiaction/entity/Notification.java index 789b92e7..005402f8 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/entity/Notification.java +++ b/src/main/java/com/server/capple/domain/notifiaction/entity/Notification.java @@ -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; } diff --git a/src/main/java/com/server/capple/domain/notifiaction/entity/NotificationLog.java b/src/main/java/com/server/capple/domain/notifiaction/entity/NotificationLog.java new file mode 100644 index 00000000..879e9b21 --- /dev/null +++ b/src/main/java/com/server/capple/domain/notifiaction/entity/NotificationLog.java @@ -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; +} diff --git a/src/main/java/com/server/capple/domain/notifiaction/entity/NotificationType.java b/src/main/java/com/server/capple/domain/notifiaction/entity/NotificationType.java index 0b7735c2..5a9e37ee 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/entity/NotificationType.java +++ b/src/main/java/com/server/capple/domain/notifiaction/entity/NotificationType.java @@ -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; } diff --git a/src/main/java/com/server/capple/domain/notifiaction/mapper/NotificationMapper.java b/src/main/java/com/server/capple/domain/notifiaction/mapper/NotificationMapper.java index 76698e75..5a38a52c 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/mapper/NotificationMapper.java +++ b/src/main/java/com/server/capple/domain/notifiaction/mapper/NotificationMapper.java @@ -1,28 +1,95 @@ 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() + .body(boardComment.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 toNotificationInfoSlice(Slice 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()) + .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(); } } diff --git a/src/main/java/com/server/capple/domain/notifiaction/repository/NotificationRepository.java b/src/main/java/com/server/capple/domain/notifiaction/repository/NotificationRepository.java index ca1064fe..233afc5c 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/repository/NotificationRepository.java +++ b/src/main/java/com/server/capple/domain/notifiaction/repository/NotificationRepository.java @@ -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 { - Slice findByMemberId(Long memberId, Pageable pageable, Class 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 findByMemberId(Long memberId, Pageable pageable); void deleteNotificationsByCreatedAtBefore(LocalDateTime targetTime); } diff --git a/src/main/java/com/server/capple/domain/notifiaction/scheduler/NotificationScheduler.java b/src/main/java/com/server/capple/domain/notifiaction/scheduler/NotificationScheduler.java index d4d79d49..bd23c836 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/scheduler/NotificationScheduler.java +++ b/src/main/java/com/server/capple/domain/notifiaction/scheduler/NotificationScheduler.java @@ -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); diff --git a/src/main/java/com/server/capple/domain/notifiaction/service/NotificationService.java b/src/main/java/com/server/capple/domain/notifiaction/service/NotificationService.java index fd3b276d..fc728286 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/service/NotificationService.java +++ b/src/main/java/com/server/capple/domain/notifiaction/service/NotificationService.java @@ -4,8 +4,9 @@ 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; @@ -13,6 +14,8 @@ 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 getNotifications(Member member, Pageable pageable); + void sendLiveQuestionOpenNotification(Question question); + void sendLiveQuestionCloseNotification(Question question); + SliceResponse getNotifications(Member member, Pageable pageable); void deleteNotificationsByCreatedAtBefore(LocalDateTime targetTime); } diff --git a/src/main/java/com/server/capple/domain/notifiaction/service/NotificationServiceImpl.java b/src/main/java/com/server/capple/domain/notifiaction/service/NotificationServiceImpl.java index c52e6515..22c8b3c4 100644 --- a/src/main/java/com/server/capple/domain/notifiaction/service/NotificationServiceImpl.java +++ b/src/main/java/com/server/capple/domain/notifiaction/service/NotificationServiceImpl.java @@ -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; @@ -9,8 +10,11 @@ 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.entity.NotificationLog; 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; @@ -39,7 +43,10 @@ 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(), + notificationMapper.toNotificationLog(board), + BOARD_HEART); notificationRepository.save(notification); } @@ -47,6 +54,7 @@ public void sendBoardHeartNotification(Long actorId, Board board) { @Transactional public void sendBoardCommentNotification(Long actorId, Board board, BoardComment boardComment) { List subscribers = boardSubscribeMemberService.findBoardSubscribeMembers(board.getId()); + NotificationLog notificationLog = notificationMapper.toNotificationLog(board, boardComment); List subscriberIds = subscribers.stream() .map(Member::getId) .filter(id -> !id.equals(actorId)) @@ -59,20 +67,26 @@ 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, + notificationLog, + 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 notifications = subscriberIds.stream() - .map(subscriberId -> notificationMapper.toNotification(subscriberId, boardCommentNotificationBody)) + .map(subscriberId -> notificationMapper.toNotification( + subscriberId, + notificationLog, + BOARD_COMMENT_DUPLICATE)) .toList(); notificationRepository.saveAll(notifications); } @@ -86,13 +100,43 @@ 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(), + notificationMapper.toNotificationLog(board, boardComment), + BOARD_COMMENT_HEART); notificationRepository.save(notification); } @Override - public Slice 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( + notificationMapper.toNotificationLog(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( + notificationMapper.toNotificationLog(question), + TODAY_QUESTION_CLOSED); + notificationRepository.save(notification); + } + + @Override + public SliceResponse getNotifications(Member member, Pageable pageable) { + Slice notificationRDBInfos = notificationRepository.findByMemberId(member.getId(), pageable); + return notificationMapper.toNotificationInfoSlice(notificationRDBInfos); } @Override diff --git a/src/main/java/com/server/capple/domain/question/scheduler/QuestionScheduler.java b/src/main/java/com/server/capple/domain/question/scheduler/QuestionScheduler.java index 8d4516b3..b20d188b 100644 --- a/src/main/java/com/server/capple/domain/question/scheduler/QuestionScheduler.java +++ b/src/main/java/com/server/capple/domain/question/scheduler/QuestionScheduler.java @@ -1,5 +1,7 @@ package com.server.capple.domain.question.scheduler; +import com.server.capple.domain.notifiaction.service.NotificationService; +import com.server.capple.domain.question.entity.Question; import com.server.capple.domain.question.service.AdminQuestionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -11,17 +13,21 @@ @Slf4j public class QuestionScheduler { private final AdminQuestionService adminQuestionService; + private final NotificationService notificationService; //초 분 시 일 월 요일 @Scheduled(cron = "0 0 7,18 * * *") //매일 오전 7시에, 오후 6시에 public void setLiveQuestion() { - adminQuestionService.setLiveQuestion(); + Question question = adminQuestionService.setLiveQuestion(); + notificationService.sendLiveQuestionOpenNotification(question); log.info("live question이 등록되었습니다."); } @Scheduled(cron = "0 0 1,14 * * *") //매일 오전 1시에, 오후 14시에 public void closeLiveQuestion() { //question을 닫음 + Question question = adminQuestionService.closeLiveQuestion(); + notificationService.sendLiveQuestionCloseNotification(question); log.info("live question이 닫혔습니다."); } } diff --git a/src/main/java/com/server/capple/domain/question/service/AdminQuestionService.java b/src/main/java/com/server/capple/domain/question/service/AdminQuestionService.java index 98565743..ba5573a3 100644 --- a/src/main/java/com/server/capple/domain/question/service/AdminQuestionService.java +++ b/src/main/java/com/server/capple/domain/question/service/AdminQuestionService.java @@ -2,6 +2,7 @@ import com.server.capple.domain.question.dto.request.QuestionRequest.QuestionCreate; import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionId; +import com.server.capple.domain.question.entity.Question; public interface AdminQuestionService { @@ -9,7 +10,7 @@ public interface AdminQuestionService { QuestionId deleteQuestion(Long questionId); - QuestionId setLiveQuestion(); - QuestionId closeLiveQuestion(); + Question setLiveQuestion(); + Question closeLiveQuestion(); } diff --git a/src/main/java/com/server/capple/domain/question/service/AdminQuestionServiceImpl.java b/src/main/java/com/server/capple/domain/question/service/AdminQuestionServiceImpl.java index f3de2f76..5319a6e6 100644 --- a/src/main/java/com/server/capple/domain/question/service/AdminQuestionServiceImpl.java +++ b/src/main/java/com/server/capple/domain/question/service/AdminQuestionServiceImpl.java @@ -39,22 +39,22 @@ public QuestionId deleteQuestion(Long questionId) { } @Transactional - public QuestionId setLiveQuestion() { + public Question setLiveQuestion() { Question newQuestion = adminQuestionRepository.findFirstByQuestionStatusOrderByIdAsc(QuestionStatus.PENDING) .orElseThrow(() -> new RestApiException(QuestionErrorCode.QUESTION_PENDING_NOT_FOUND)); newQuestion.setQuestionStatus(QuestionStatus.LIVE); - return new QuestionId(newQuestion.getId()); + return newQuestion; } @Transactional - public QuestionId closeLiveQuestion() { + public Question closeLiveQuestion() { Question question = adminQuestionRepository.findFirstByQuestionStatusOrderByIdAsc(QuestionStatus.LIVE) .orElseThrow(() -> new RestApiException(QuestionErrorCode.QUESTION_LIVE_NOT_FOUND)); question.setQuestionStatus(QuestionStatus.OLD); - return new QuestionId(question.getId()); + return question; } } diff --git a/src/main/resources/templates/certCodeMail.html b/src/main/resources/templates/certCodeMail.html index 9b0b5005..d877ceb1 100644 --- a/src/main/resources/templates/certCodeMail.html +++ b/src/main/resources/templates/certCodeMail.html @@ -2,30 +2,33 @@ + -
- - +
+ + - - + + - + - - Capple + + + Qapple
-
- Capple 회원가입을 위한 인증번호입니다. +
+ Qapple 회원가입을 위한 인증번호입니다.
-
아래 코드를 회원가입 창으로 돌아가 입력해주세요.
+
+
아래 코드를 회원가입 창으로 돌아가 입력해주세요.

- 인증 코드 - + 인증 코드 + 12345
diff --git a/src/test/java/com/server/capple/domain/question/service/QuestionServiceTest.java b/src/test/java/com/server/capple/domain/question/service/QuestionServiceTest.java index f995b583..4e320234 100644 --- a/src/test/java/com/server/capple/domain/question/service/QuestionServiceTest.java +++ b/src/test/java/com/server/capple/domain/question/service/QuestionServiceTest.java @@ -1,6 +1,5 @@ package com.server.capple.domain.question.service; -import com.server.capple.domain.question.dto.response.QuestionResponse; import com.server.capple.domain.question.dto.response.QuestionResponse.QuestionInfo; import com.server.capple.domain.question.entity.Question; import com.server.capple.domain.question.entity.QuestionStatus; @@ -30,8 +29,8 @@ public class QuestionServiceTest extends ServiceTestConfig { @Transactional public void setLiveQuestionTest() { //given & when - QuestionResponse.QuestionId questionId = adminQuestionService.setLiveQuestion(); - Question question = questionService.findQuestion(questionId.getQuestionId()); + Long questionId = adminQuestionService.setLiveQuestion().getId(); + Question question = questionService.findQuestion(questionId); //then assertEquals(question.getContent(), "가장 좋아하는 음식은 무엇인가요?"); @@ -43,8 +42,8 @@ public void setLiveQuestionTest() { @Transactional public void closeLiveQuestionTest() { //given & when - QuestionResponse.QuestionId questionId = adminQuestionService.closeLiveQuestion(); - Question question = questionService.findQuestion(questionId.getQuestionId()); + Long questionId = adminQuestionService.closeLiveQuestion().getId(); + Question question = questionService.findQuestion(questionId); //then assertEquals(question.getContent(), "아카데미 러너 중 가장 마음에 드는 유형이 있나요?");