Skip to content

Commit

Permalink
Merge pull request #181 from Team-Capple/develop
Browse files Browse the repository at this point in the history
[RELEASE] 회원가입 메일 개선, 질문 생성/종료 원격 알림 이관, 기존 원격 알림 형식 변경
  • Loading branch information
jaewonLeeKOR authored Sep 24, 2024
2 parents 19884b3 + 5b8940c commit 1e2fbda
Show file tree
Hide file tree
Showing 23 changed files with 298 additions and 76 deletions.
2 changes: 2 additions & 0 deletions src/main/java/com/server/capple/CappleApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +19,7 @@
@EnableConfigurationProperties
@EnableScheduling
@EnableCaching
@EnableAsync
public class CappleApplication {

public static void main(String[] args) {
Expand Down
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 @@ -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();
Expand Down Expand Up @@ -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;
}
}
}
}
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 @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> sendMailAddressCertificationMail(String receiver, Boolean isWhiteList);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,19 +25,21 @@ 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<String> 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");
mimeMessageHelper.setTo(receiver);
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);
}
}
Expand All @@ -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;
Expand Down
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,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<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())
.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();
}
}
Loading

0 comments on commit 1e2fbda

Please sign in to comment.