diff --git a/build.gradle b/build.gradle index 89c87e6..d4bceb2 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,10 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-gcp-starter-storage:1.2.8.RELEASE' implementation 'net.coobird:thumbnailator:0.4.14' + // FCM + implementation 'com.google.firebase:firebase-admin:6.8.1' + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2' + // STOMP implementation 'org.webjars:webjars-locator-core' implementation 'org.webjars:sockjs-client:1.5.1' @@ -69,6 +73,7 @@ dependencies { // jackson 날짜/시간 직렬화 implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + } tasks.named('bootBuildImage') { diff --git a/src/main/java/com/favoriteplace/app/controller/FCMNotificationController.java b/src/main/java/com/favoriteplace/app/controller/FCMNotificationController.java new file mode 100644 index 0000000..55b18bd --- /dev/null +++ b/src/main/java/com/favoriteplace/app/controller/FCMNotificationController.java @@ -0,0 +1,42 @@ +package com.favoriteplace.app.controller; + +import com.favoriteplace.app.service.fcm.FCMNotificationService; +import com.favoriteplace.app.service.fcm.dto.PostTokenCond; +import com.favoriteplace.app.service.fcm.enums.TokenMessage; +import com.favoriteplace.app.service.fcm.enums.TotalTopicMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/fcm") +@RequiredArgsConstructor +public class FCMNotificationController { + private final FCMNotificationService fcmNotificationService; + + @PostMapping("/token") + public String sendNotificationByToken( + @RequestParam String token + ){ + return fcmNotificationService.sendNotificationByToken(PostTokenCond.builder() + .token(token).postId(1L).tokenMessage(TokenMessage.POST_NEW_COMMENT).message("댓글 내용") + .build()); + } + + @PostMapping("/topic/subscribe") + public String subScribeTopic( + @RequestParam String token + ){ + fcmNotificationService.subscribeTopic("total", token); + return "토픽에 정상적으로 등록 완료"; + } + + @PostMapping("/topic/send") + public String sendAlarmByTopic( + + ){ + return fcmNotificationService.sendTotalAlarmByTopic(TotalTopicMessage.INFORM); + } +} diff --git a/src/main/java/com/favoriteplace/app/controller/GuestBookCommentController.java b/src/main/java/com/favoriteplace/app/controller/GuestBookCommentController.java index 8eeed6e..db2d353 100644 --- a/src/main/java/com/favoriteplace/app/controller/GuestBookCommentController.java +++ b/src/main/java/com/favoriteplace/app/controller/GuestBookCommentController.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.parameters.P; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -51,13 +52,22 @@ public ResponseEntity createGuestBookComment ){ Member member = securityUtil.getUser(); // Member member = memberRepository.findById(1L).orElseThrow(() -> new RestApiException(ErrorCode.USER_NOT_FOUND)); - commentCommandService.createGuestBookComment(member, guestbookId, guestBookCommentDto); + Long commentId = commentCommandService.createGuestBookComment(member, guestbookId, guestBookCommentDto); return new ResponseEntity<>( - PostResponseDto.SuccessResponseDto.builder().message("댓글이 성공적으로 등록했습니다.").build(), + PostResponseDto.SuccessResponseDto.builder().commentId(commentId).message("댓글이 성공적으로 등록했습니다.").build(), HttpStatus.OK ); } + @PostMapping("/{guestbook_id}/comments/{comment_id}/notification") + public ResponseEntity sendGuestBookNotification( + @PathVariable("guestbook_id") Long guestbookId, + @PathVariable("comment_id") Long commentId + ){ + commentCommandService.sendGuestBookNotification(guestbookId, commentId); + return ResponseEntity.ok().build(); + } + @PutMapping("/comments/{comment_id}") public ResponseEntity modifyGuestBookComment( @PathVariable("comment_id") Long commentId, diff --git a/src/main/java/com/favoriteplace/app/controller/MyPageController.java b/src/main/java/com/favoriteplace/app/controller/MyPageController.java index 0a74d5b..c04ca4b 100644 --- a/src/main/java/com/favoriteplace/app/controller/MyPageController.java +++ b/src/main/java/com/favoriteplace/app/controller/MyPageController.java @@ -3,11 +3,14 @@ import com.favoriteplace.app.domain.Member; import com.favoriteplace.app.dto.CommonResponseDto; import com.favoriteplace.app.dto.MyPageDto; +import com.favoriteplace.app.dto.MyPageDto.MyFcmTokenDto; import com.favoriteplace.app.dto.community.CommentResponseDto; import com.favoriteplace.app.service.MyPageCommandService; import com.favoriteplace.app.service.MyPageQueryService; import com.favoriteplace.global.util.SecurityUtil; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -84,4 +87,14 @@ public MyPageDto.MyModifyBlockDto modifyMemberBlock( Member member = securityUtil.getUser(); return myPageCommandService.modifyMemberBlock(member, blockedMember); } + + //FCM token 등록 & 변경 + @PatchMapping("/fcmToken") + public ResponseEntity modifyFcmToken( + @Valid @RequestBody MyFcmTokenDto request + ){ + Member member = securityUtil.getUser(); + myPageCommandService.modifyFcmToken(member, request); + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/src/main/java/com/favoriteplace/app/controller/NotificationController.java b/src/main/java/com/favoriteplace/app/controller/NotificationController.java new file mode 100644 index 0000000..a4e0497 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/controller/NotificationController.java @@ -0,0 +1,60 @@ +package com.favoriteplace.app.controller; + +import com.favoriteplace.app.domain.Member; +import com.favoriteplace.app.dto.NotificationResponseDto; +import com.favoriteplace.app.service.NotificationService; +import com.favoriteplace.global.util.SecurityUtil; +import jakarta.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/notifications") +@RequiredArgsConstructor +@Validated +public class NotificationController { + private final NotificationService notificationService; + private final SecurityUtil securityUtil; + + // 알림 전체 조회 + @GetMapping() + public ResponseEntity getAllNotification( + @Min(value = 1, message = "page는 1 이상입니다.") @RequestParam(required = false, defaultValue = "1") Integer page, + @Min(value = 1, message = "size는 1 이상입니다.") @RequestParam(required = false, defaultValue = "1") Integer size + ){ + Member member = securityUtil.getUser(); + NotificationResponseDto response = notificationService.getAllNotification(member, page, size); + return ResponseEntity.ok(response); + } + + // 알림 한번에 다 읽음 처리 + @PatchMapping() + public ResponseEntity readAllNotification(){ + Member member = securityUtil.getUser(); + notificationService.readAllNotification(member); + return ResponseEntity.noContent().build(); + } + + // 특정 알림 읽음 처리 + @PatchMapping("/{notificationId}") + public ResponseEntity readNotification( + @PathVariable Long notificationId + ){ + Member member = securityUtil.getUser(); + notificationService.readNotification(notificationId, member); + return ResponseEntity.noContent().build(); + } + + // 특정 알림 삭제 + @DeleteMapping("/{notificationId}") + public ResponseEntity deleteNotification( + @PathVariable Long notificationId + ){ + Member member = securityUtil.getUser(); + notificationService.deleteNotification(notificationId, member); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/com/favoriteplace/app/controller/PilgrimageApiController.java b/src/main/java/com/favoriteplace/app/controller/PilgrimageApiController.java index ed8a638..f46f8cf 100644 --- a/src/main/java/com/favoriteplace/app/controller/PilgrimageApiController.java +++ b/src/main/java/com/favoriteplace/app/controller/PilgrimageApiController.java @@ -1,10 +1,7 @@ package com.favoriteplace.app.controller; import com.favoriteplace.app.domain.Member; -import com.favoriteplace.app.domain.travel.Rally; import com.favoriteplace.app.dto.CommonResponseDto; -import com.favoriteplace.app.dto.community.GuestBookRequestDto; -import com.favoriteplace.app.dto.community.PostResponseDto; import com.favoriteplace.app.dto.travel.PilgrimageDto; import com.favoriteplace.app.dto.travel.RallyDto; import com.favoriteplace.app.service.PilgrimageCommandService; @@ -15,12 +12,9 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; import java.util.List; @RestController @@ -107,6 +101,22 @@ public CommonResponseDto.PostResponseDto likeToRally(@PathVariable("rally_id")Lo return pilgrimageCommandService.likeToRally(rallyId, member); } + // 랠리 FCM 구독 + @PostMapping("/{rally_id}/subscribe") + public ResponseEntity subscribeRally(@PathVariable("rally_id") Long rallyId){ + Member member = securityUtil.getUser(); + pilgrimageCommandService.subscribeRally(rallyId, member); + return ResponseEntity.ok().build(); + } + + // 랠리 FCM 구독 취소 + @DeleteMapping("/{rally_id}/unsubscribe") + public ResponseEntity unsubscribeRally(@PathVariable("rally_id") Long rallyId){ + Member member = securityUtil.getUser(); + pilgrimageCommandService.unsubscribeRally(rallyId, member); + return ResponseEntity.noContent().build(); + } + // 성지순례 장소 방문 인증하기 // @PostMapping("/certified/{pilgrimage_id}") // public CommonResponseDto.RallyResponseDto certifyToPilgrimage( diff --git a/src/main/java/com/favoriteplace/app/controller/PilgrimageSocketController.java b/src/main/java/com/favoriteplace/app/controller/PilgrimageSocketController.java index 16b1487..0a93feb 100644 --- a/src/main/java/com/favoriteplace/app/controller/PilgrimageSocketController.java +++ b/src/main/java/com/favoriteplace/app/controller/PilgrimageSocketController.java @@ -24,18 +24,17 @@ public class PilgrimageSocketController { private final PilgrimageCommandService pilgrimageService; /** - * 위도/경도 전달 시 상태 변경 알리는 컨트롤러 + * 최초 접근 시 버튼 상태 전달하는 컨트롤러 * - * 요청 컨트롤러 /app/location/{pilgrimageId} - * 응답 컨트롤러 /pub/statusUpdate/{pilgrimageId} + * 요청 컨트롤러 /app/connect/{pilgrimageId} + * 응답 컨트롤러 /pub/statusUpdate/{pilgrimageId} * * @param pilgrimageId 성지순례 ID - * @param userLocation 위도/경도 - * @return 버튼 상태 json + * @return */ - @MessageMapping("/location/{pilgrimageId}") + @MessageMapping("/connect/{pilgrimageId}") @SendTo("/pub/statusUpdate/{pilgrimageId}") - public PilgrimageSocketDto.ButtonState checkUserLocation(@DestinationVariable Long pilgrimageId, Principal principal, PilgrimageDto.PilgrimageCertifyRequestDto userLocation) { + public PilgrimageSocketDto.ButtonState sendInitialStatus(@DestinationVariable Long pilgrimageId, Principal principal) { if (principal == null) throw new RestApiException(ErrorCode.USER_NOT_AUTHOR); @@ -43,21 +42,23 @@ public PilgrimageSocketDto.ButtonState checkUserLocation(@DestinationVariable Lo ((UsernamePasswordAuthenticationToken) principal).getPrincipal(); Member member = userDetails.getMember(); - return pilgrimageService.buttonStatusUpdate(pilgrimageId, userLocation, member); + PilgrimageSocketDto.ButtonState buttonState = pilgrimageService.initButton(member, pilgrimageId); + return buttonState; } /** - * 최초 접근 시 버튼 상태 전달하는 컨트롤러 + * 위도/경도 전달 시 상태 변경 알리는 컨트롤러 * - * 요청 컨트롤러 /app/connect/{pilgrimageId} - * 응답 컨트롤러 /pub/statusUpdate/{pilgrimageId} + * 요청 컨트롤러 /app/location/{pilgrimageId} + * 응답 컨트롤러 /pub/statusUpdate/{pilgrimageId} * * @param pilgrimageId 성지순례 ID - * @return + * @param userLocation 위도/경도 + * @return 버튼 상태 json */ - @MessageMapping("/connect/{pilgrimageId}") + @MessageMapping("/location/{pilgrimageId}") @SendTo("/pub/statusUpdate/{pilgrimageId}") - public PilgrimageSocketDto.ButtonState sendInitialStatus(@DestinationVariable Long pilgrimageId, Principal principal) { + public PilgrimageSocketDto.ButtonState checkUserLocation(@DestinationVariable Long pilgrimageId, Principal principal, PilgrimageDto.PilgrimageCertifyRequestDto userLocation) { if (principal == null) throw new RestApiException(ErrorCode.USER_NOT_AUTHOR); @@ -65,7 +66,8 @@ public PilgrimageSocketDto.ButtonState sendInitialStatus(@DestinationVariable Lo ((UsernamePasswordAuthenticationToken) principal).getPrincipal(); Member member = userDetails.getMember(); - return pilgrimageService.determineButtonState(member, pilgrimageId); + PilgrimageSocketDto.ButtonState buttonState = pilgrimageService.buttonStatusUpdate(pilgrimageId, userLocation, member); + return buttonState; } /** diff --git a/src/main/java/com/favoriteplace/app/controller/PostCommentController.java b/src/main/java/com/favoriteplace/app/controller/PostCommentController.java index ca33925..4032d33 100644 --- a/src/main/java/com/favoriteplace/app/controller/PostCommentController.java +++ b/src/main/java/com/favoriteplace/app/controller/PostCommentController.java @@ -18,6 +18,8 @@ import java.util.List; +import static com.favoriteplace.app.dto.community.PostResponseDto.*; + @RestController @RequestMapping("/posts/free") @RequiredArgsConstructor @@ -29,7 +31,7 @@ public class PostCommentController { private final MemberRepository memberRepository; @GetMapping("/my-comments") - public ResponseEntity getMyComments( + public ResponseEntity getMyComments( @RequestParam(required = false, defaultValue = "1") int page, @RequestParam(required = false, defaultValue = "10") int size ){ @@ -50,21 +52,30 @@ public ResponseEntity getPostComments( } @PostMapping("/{post_id}/comments") - public ResponseEntity createPostComment( - @PathVariable("post_id") long postId, + public ResponseEntity createPostComment( + @PathVariable("post_id") Long postId, @RequestBody CommentRequestDto.CreateComment dto ){ Member member = securityUtil.getUser(); // Member member = memberRepository.findById(1L).orElseThrow(() -> new RestApiException(ErrorCode.USER_NOT_FOUND)); - commentCommandService.createPostComment(member, postId, dto); + Long commentId = commentCommandService.createPostComment(member, postId, dto); return new ResponseEntity<>( - PostResponseDto.SuccessResponseDto.builder().message("댓글을 성공적으로 등록했습니다.").build(), + SuccessResponseDto.builder().commentId(commentId).message("댓글을 성공적으로 등록했습니다.").build(), HttpStatus.OK ); } + @PostMapping("/{post_id}/comments/{comment_id}/notification") + public ResponseEntity sendPostNotification( + @PathVariable("post_id") long postId, + @PathVariable("comment_id") long commentId + ){ + commentCommandService.sendPostNotification(postId, commentId); + return ResponseEntity.ok().build(); + } + @PutMapping("/comments/{comment_id}") - public ResponseEntity modifyPostComment( + public ResponseEntity modifyPostComment( @PathVariable("comment_id") long commentId, @RequestBody CommentRequestDto.ModifyComment dto ){ @@ -72,20 +83,20 @@ public ResponseEntity modifyPostComment( // Member member = memberRepository.findById(1L).orElseThrow(() -> new RestApiException(ErrorCode.USER_NOT_FOUND)); commentCommandService.modifyComment(member, commentId, dto.getContent()); return new ResponseEntity<>( - PostResponseDto.SuccessResponseDto.builder().message("댓글을 성공적으로 수정했습니다.").build(), + SuccessResponseDto.builder().message("댓글을 성공적으로 수정했습니다.").build(), HttpStatus.OK ); } @DeleteMapping("/comments/{comment_id}") - public ResponseEntity deletePostComment( + public ResponseEntity deletePostComment( @PathVariable("comment_id") long commentId ){ Member member = securityUtil.getUser(); // Member member = memberRepository.findById(1L).orElseThrow(() -> new RestApiException(ErrorCode.USER_NOT_FOUND)); commentCommandService.deleteComment(member, commentId); return new ResponseEntity<>( - PostResponseDto.SuccessResponseDto.builder().message("댓글을 성공적으로 삭제했습니다.").build(), + SuccessResponseDto.builder().message("댓글을 성공적으로 삭제했습니다.").build(), HttpStatus.OK ); } diff --git a/src/main/java/com/favoriteplace/app/converter/FcmConverter.java b/src/main/java/com/favoriteplace/app/converter/FcmConverter.java new file mode 100644 index 0000000..910f642 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/converter/FcmConverter.java @@ -0,0 +1,92 @@ +package com.favoriteplace.app.converter; + +import com.favoriteplace.app.domain.community.Comment; +import com.favoriteplace.app.domain.community.GuestBook; +import com.favoriteplace.app.domain.community.Post; +import com.favoriteplace.app.service.fcm.dto.PostTokenCond; +import com.favoriteplace.app.service.fcm.enums.TokenMessage; +import com.favoriteplace.global.exception.ErrorCode; +import com.favoriteplace.global.exception.RestApiException; + +public class FcmConverter { + + /** + * 게시글 작성자에게 알림 전송 + */ + public static PostTokenCond toPostWriter(Post post, Comment newComment){ + if(post.getMember().getFcmToken() == null){ + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + return PostTokenCond.builder() + .token(post.getMember().getFcmToken()) + .tokenMessage(TokenMessage.POST_NEW_COMMENT) + .postId(post.getId()) + .message(newComment.getContent()) + .build(); + } + + /** + * 성지순례 인증글 작성자에게 알림 전송 + */ + public static PostTokenCond toGuestBookWriter(GuestBook guestBook, Comment newComment){ + if(guestBook.getMember().getFcmToken() == null){ + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + return PostTokenCond.builder() + .token(guestBook.getMember().getFcmToken()) + .tokenMessage(TokenMessage.GUESTBOOK_NEW_COMMENT) + .guestBookId(guestBook.getId()) + .message(newComment.getContent()) + .build(); + } + + public static PostTokenCond toParentCommentWriter(Post post, Comment newComment){ + if(newComment.getParentComment().getMember().getFcmToken() == null){ + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + return PostTokenCond.builder() + .token(newComment.getParentComment().getMember().getFcmToken()) + .tokenMessage(TokenMessage.POST_COMMENT_NEW_SUBCOMMENT) + .postId(post.getId()) + .message(newComment.getContent()) + .build(); + } + + public static PostTokenCond toReferCommentWriter(Post post, Comment newComment){ + if(newComment.getReferenceComment().getMember().getFcmToken() == null){ + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + return PostTokenCond.builder() + .token(newComment.getReferenceComment().getMember().getFcmToken()) + .tokenMessage(TokenMessage.POST_COMMENT_NEW_SUBCOMMENT) + .postId(post.getId()) + .message(newComment.getContent()) + .build(); + } + + public static PostTokenCond toParentCommentWriter(GuestBook guestBook, Comment newComment){ + if(newComment.getParentComment().getMember().getFcmToken() == null){ + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + return PostTokenCond.builder() + .token(newComment.getParentComment().getMember().getFcmToken()) + .tokenMessage(TokenMessage.GUESTBOOK_COMMENT_NEW_SUBCOMMENT) + .guestBookId(guestBook.getId()) + .message(newComment.getContent()) + .build(); + } + + public static PostTokenCond toReferCommentWriter(GuestBook guestBook, Comment newComment){ + if(newComment.getReferenceComment().getMember().getFcmToken() == null){ + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + return PostTokenCond.builder() + .token(newComment.getReferenceComment().getMember().getFcmToken()) + .tokenMessage(TokenMessage.GUESTBOOK_COMMENT_NEW_SUBCOMMENT) + .guestBookId(guestBook.getId()) + .message(newComment.getContent()) + .build(); + } + + +} diff --git a/src/main/java/com/favoriteplace/app/converter/NotificationConverter.java b/src/main/java/com/favoriteplace/app/converter/NotificationConverter.java new file mode 100644 index 0000000..7ab1a4a --- /dev/null +++ b/src/main/java/com/favoriteplace/app/converter/NotificationConverter.java @@ -0,0 +1,97 @@ +package com.favoriteplace.app.converter; + +import com.favoriteplace.app.domain.Member; +import com.favoriteplace.app.domain.Notification; +import com.favoriteplace.app.domain.community.Comment; +import com.favoriteplace.app.domain.community.GuestBook; +import com.favoriteplace.app.domain.community.Post; +import com.favoriteplace.app.dto.NotificationResponseDto; +import com.favoriteplace.app.dto.NotificationResponseDto.NotificationInfo; +import com.favoriteplace.app.service.fcm.enums.TokenMessage; +import com.favoriteplace.global.util.DateTimeFormatUtils; +import org.springframework.data.domain.Page; + +public class NotificationConverter { + public static NotificationResponseDto toNotificationResponseDto(Page notifications){ + return NotificationResponseDto.builder() + .page(notifications.getNumber()) + .size(notifications.getSize()) + .notifications( + notifications.getContent().stream() + .map(n -> NotificationInfo.builder() + .id(n.getId()) + .type(n.getType()) + .date(DateTimeFormatUtils.getPassDateTime(n.getCreatedAt())) + .title(n.getTitle()) + .content(n.getContent()) + .postId(n.getPostId()) + .guestBookId(n.getGuestBookId()) + .rallyId(n.getRallyId()) + .isRead(n.getIsRead()) + .build()) + .toList() + ) + .build(); + } + + public static Notification toPostNewComment(Post post, Comment comment){ + return Notification.builder() + .type(TokenMessage.POST_NEW_COMMENT.getType()) + .title(TokenMessage.POST_NEW_COMMENT.getTitle()) + .content(comment.getContent()) + .postId(post.getId()) + .member(post.getMember()) + .build(); + } + + public static Notification toPostParentNewSubComment(Post post, Comment comment){ + return Notification.builder() + .type(TokenMessage.POST_COMMENT_NEW_SUBCOMMENT.getType()) + .title(TokenMessage.POST_COMMENT_NEW_SUBCOMMENT.getTitle()) + .content(comment.getContent()) + .postId(post.getId()) + .member(comment.getParentComment().getMember()) + .build(); + } + + public static Notification toPostReferNewSubComment(Post post, Comment comment){ + return Notification.builder() + .type(TokenMessage.POST_COMMENT_NEW_SUBCOMMENT.getType()) + .title(TokenMessage.POST_COMMENT_NEW_SUBCOMMENT.getTitle()) + .content(comment.getContent()) + .postId(post.getId()) + .member(comment.getReferenceComment().getMember()) + .build(); + } + + public static Notification toGuestBookNewComment(GuestBook guestBook, Comment comment){ + return Notification.builder() + .type(TokenMessage.GUESTBOOK_NEW_COMMENT.getType()) + .title(TokenMessage.GUESTBOOK_NEW_COMMENT.getTitle()) + .content(comment.getContent()) + .guestBookId(guestBook.getId()) + .member(guestBook.getMember()) + .build(); + } + + public static Notification toGuestBookParentNewSubComment(GuestBook guestBook, Comment comment){ + return Notification.builder() + .type(TokenMessage.GUESTBOOK_COMMENT_NEW_SUBCOMMENT.getType()) + .title(TokenMessage.GUESTBOOK_COMMENT_NEW_SUBCOMMENT.getTitle()) + .content(comment.getContent()) + .guestBookId(guestBook.getId()) + .member(comment.getParentComment().getMember()) + .build(); + } + + public static Notification toGuestBookReferNewSubComment(GuestBook guestBook, Comment comment){ + return Notification.builder() + .type(TokenMessage.GUESTBOOK_COMMENT_NEW_SUBCOMMENT.getType()) + .title(TokenMessage.GUESTBOOK_COMMENT_NEW_SUBCOMMENT.getTitle()) + .content(comment.getContent()) + .guestBookId(guestBook.getId()) + .member(comment.getReferenceComment().getMember()) + .build(); + } + +} diff --git a/src/main/java/com/favoriteplace/app/domain/Member.java b/src/main/java/com/favoriteplace/app/domain/Member.java index 10422ef..32b9625 100644 --- a/src/main/java/com/favoriteplace/app/domain/Member.java +++ b/src/main/java/com/favoriteplace/app/domain/Member.java @@ -73,6 +73,8 @@ public class Member extends BaseTimeEntity { private String refreshToken; + private String fcmToken; + public void updatePassword(String password) { this.password = password; } public void updateRefreshToken(String refreshToken) { @@ -98,4 +100,5 @@ public void updateIcon(Item icon) { public void updateTitle(Item title) { this.profileTitle = title; } + public void refreshFcmToken(String fcmToken){this.fcmToken = fcmToken;} } diff --git a/src/main/java/com/favoriteplace/app/domain/Notification.java b/src/main/java/com/favoriteplace/app/domain/Notification.java new file mode 100644 index 0000000..614deea --- /dev/null +++ b/src/main/java/com/favoriteplace/app/domain/Notification.java @@ -0,0 +1,44 @@ +package com.favoriteplace.app.domain; + +import com.favoriteplace.app.domain.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Notification extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String type; + @Column(nullable = false) + private String title; + private String content; + private Long postId; + private Long guestBookId; + private Long rallyId; + private Boolean isRead; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Builder + private Notification(String type, String title, String content, Long postId, Long guestBookId, Long rallyId, Member member){ + this.type = type; + this.title = title; + this.content = content; + this.postId = postId; + this.guestBookId = guestBookId; + this.rallyId = rallyId; + this.isRead = false; + this.member = member; + } + + public void readNotification(){this.isRead = true;} +} diff --git a/src/main/java/com/favoriteplace/app/dto/MyPageDto.java b/src/main/java/com/favoriteplace/app/dto/MyPageDto.java index 3efbc2a..2a032cc 100644 --- a/src/main/java/com/favoriteplace/app/dto/MyPageDto.java +++ b/src/main/java/com/favoriteplace/app/dto/MyPageDto.java @@ -1,5 +1,6 @@ package com.favoriteplace.app.dto; +import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -85,4 +86,13 @@ public static class MyBlockDto { public static class MyModifyBlockDto{ Boolean isBlocked; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class MyFcmTokenDto{ + @NotEmpty + String fcmToken; + } } diff --git a/src/main/java/com/favoriteplace/app/dto/NotificationResponseDto.java b/src/main/java/com/favoriteplace/app/dto/NotificationResponseDto.java new file mode 100644 index 0000000..1c54d96 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/dto/NotificationResponseDto.java @@ -0,0 +1,25 @@ +package com.favoriteplace.app.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record NotificationResponseDto( + Integer page, + Integer size, + List notifications +) { + @Builder + public record NotificationInfo( + Long id, + String type, + String date, + String title, + String content, + Long postId, + Long guestBookId, + Long rallyId, + Boolean isRead + ){ } +} diff --git a/src/main/java/com/favoriteplace/app/dto/community/PostResponseDto.java b/src/main/java/com/favoriteplace/app/dto/community/PostResponseDto.java index d2f0cff..c73f4ba 100644 --- a/src/main/java/com/favoriteplace/app/dto/community/PostResponseDto.java +++ b/src/main/java/com/favoriteplace/app/dto/community/PostResponseDto.java @@ -1,8 +1,6 @@ package com.favoriteplace.app.dto.community; -import com.favoriteplace.app.domain.community.Post; import com.favoriteplace.app.dto.UserInfoResponseDto; -import com.favoriteplace.global.util.DateTimeFormatUtils; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -17,6 +15,7 @@ public class PostResponseDto { @NoArgsConstructor @AllArgsConstructor public static class SuccessResponseDto{ + private Long commentId; private String message; } diff --git a/src/main/java/com/favoriteplace/app/dto/fcm/FcmMessage.java b/src/main/java/com/favoriteplace/app/dto/fcm/FcmMessage.java new file mode 100644 index 0000000..5bc1b57 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/dto/fcm/FcmMessage.java @@ -0,0 +1,30 @@ +package com.favoriteplace.app.dto.fcm; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class FcmMessage { + private final boolean validateOnly; + private final Message message; + + private FcmMessage(Message message){ + this.validateOnly = false; + this.message = message; + } + + @Getter + @Builder + public static class Message{ + private Notification notification; + private String token; + } + + @Getter + @Builder + public static class Notification{ + private String title; + private String body; + private String image; + } +} diff --git a/src/main/java/com/favoriteplace/app/dto/fcm/FcmNotificationRequest.java b/src/main/java/com/favoriteplace/app/dto/fcm/FcmNotificationRequest.java new file mode 100644 index 0000000..9f2e9b6 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/dto/fcm/FcmNotificationRequest.java @@ -0,0 +1,22 @@ +package com.favoriteplace.app.dto.fcm; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class FcmNotificationRequest { + private Long targetUserId; + private String title; + private String body; + // private String image; + // private Map data + + @Builder + private FcmNotificationRequest(Long targetUserId, String title, String body){ + this.targetUserId = targetUserId; + this.title = title; + this.body = body; + } +} diff --git a/src/main/java/com/favoriteplace/app/repository/LikedRallyRepository.java b/src/main/java/com/favoriteplace/app/repository/LikedRallyRepository.java index 425be6e..be8e228 100644 --- a/src/main/java/com/favoriteplace/app/repository/LikedRallyRepository.java +++ b/src/main/java/com/favoriteplace/app/repository/LikedRallyRepository.java @@ -22,4 +22,9 @@ public interface LikedRallyRepository extends JpaRepository { "GROUP BY l.rally " + "ORDER BY COUNT(l) DESC") List findMonthlyTrendingRally(@Param("startDate") LocalDateTime startDate); + + @Query("SELECT DISTINCT lr.rally.id "+ + "FROM LikedRally lr " + + "WHERE lr.member.id = :memberId") + List findDistinctRallyIdsByMember(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/favoriteplace/app/repository/NotificationRepository.java b/src/main/java/com/favoriteplace/app/repository/NotificationRepository.java new file mode 100644 index 0000000..10a084c --- /dev/null +++ b/src/main/java/com/favoriteplace/app/repository/NotificationRepository.java @@ -0,0 +1,20 @@ +package com.favoriteplace.app.repository; + +import com.favoriteplace.app.domain.Notification; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + + +@Repository +public interface NotificationRepository extends JpaRepository { + @Modifying + @Query("UPDATE Notification n SET n.isRead = true WHERE n.member.id = :memberId") + void readAllNotification(@Param("memberId") Long memberId); + + Page findByMemberId(Long memberId, Pageable pageable); +} diff --git a/src/main/java/com/favoriteplace/app/service/MyPageCommandService.java b/src/main/java/com/favoriteplace/app/service/MyPageCommandService.java index 6fbb27d..5c36a68 100644 --- a/src/main/java/com/favoriteplace/app/service/MyPageCommandService.java +++ b/src/main/java/com/favoriteplace/app/service/MyPageCommandService.java @@ -7,10 +7,12 @@ import com.favoriteplace.app.domain.item.Item; import com.favoriteplace.app.dto.CommonResponseDto; import com.favoriteplace.app.dto.MyPageDto; +import com.favoriteplace.app.dto.MyPageDto.MyFcmTokenDto; import com.favoriteplace.app.repository.AcquiredItemRepository; import com.favoriteplace.app.repository.BlockRepository; import com.favoriteplace.app.repository.ItemRepository; import com.favoriteplace.app.repository.MemberRepository; +import com.favoriteplace.app.service.fcm.FCMNotificationService; import com.favoriteplace.global.exception.ErrorCode; import com.favoriteplace.global.exception.RestApiException; import lombok.RequiredArgsConstructor; @@ -24,6 +26,7 @@ public class MyPageCommandService { private final MemberRepository memberRepository; private final ItemRepository itemRepository; private final AcquiredItemRepository acquiredItemRepository; + private final FCMNotificationService fcmNotificationService; /** * 다른 유저를 차단 또는 차단 해제 @@ -56,6 +59,7 @@ public MyPageDto.MyModifyBlockDto modifyMemberBlock(Member member, Long blockedM * @param member 착용자 * @return */ + @Transactional public CommonResponseDto.PostResponseDto wearItem(Long itemId, Member member) { Item item = itemRepository.findById(itemId) .orElseThrow(()->new RestApiException(ErrorCode.ITEM_NOT_EXISTS)); @@ -69,4 +73,14 @@ public CommonResponseDto.PostResponseDto wearItem(Long itemId, Member member) { memberRepository.save(member); return CommonConverter.toPostResponseDto(true, "착용이 완료되었습니다."); } + + /** + * FCM 토큰 등록 & 변경 + * @param member 사용자 + * @param request RequestBody + */ + @Transactional + public void modifyFcmToken(Member member, MyFcmTokenDto request) { + fcmNotificationService.refreshFCMTopicAndToken(member, request.getFcmToken()); + } } diff --git a/src/main/java/com/favoriteplace/app/service/NotificationService.java b/src/main/java/com/favoriteplace/app/service/NotificationService.java new file mode 100644 index 0000000..4a1ab41 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/service/NotificationService.java @@ -0,0 +1,51 @@ +package com.favoriteplace.app.service; + +import com.favoriteplace.app.domain.Member; +import com.favoriteplace.app.domain.Notification; +import com.favoriteplace.app.dto.NotificationResponseDto; +import com.favoriteplace.app.repository.NotificationRepository; +import com.favoriteplace.global.exception.ErrorCode; +import com.favoriteplace.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.favoriteplace.app.converter.NotificationConverter.toNotificationResponseDto; + +@Service +@RequiredArgsConstructor +public class NotificationService { + private final NotificationRepository notificationRepository; + + @Transactional + public void readAllNotification(Member member) { + notificationRepository.readAllNotification(member.getId()); + } + + @Transactional + public void readNotification(Long notificationId, Member member) { + Notification notification = notificationRepository.findById(notificationId).orElseThrow(() -> new RestApiException(ErrorCode.NOTIFICATION_NOT_EXIST)); + if(member != notification.getMember()){ + throw new RestApiException(ErrorCode.NOTIFICATION_NOT_BELONG); + } + notification.readNotification(); + } + + @Transactional + public void deleteNotification(Long notificationId, Member member) { + Notification notification = notificationRepository.findById(notificationId).orElseThrow(() -> new RestApiException(ErrorCode.NOTIFICATION_NOT_EXIST)); + if(member != notification.getMember()){ + throw new RestApiException(ErrorCode.NOTIFICATION_NOT_BELONG); + } + notificationRepository.delete(notification); + } + + @Transactional + public NotificationResponseDto getAllNotification(Member member, Integer page, Integer size) { + PageRequest pageRequest = PageRequest.of(page-1, size); + Page notifications = notificationRepository.findByMemberId(member.getId(), pageRequest); + return toNotificationResponseDto(notifications); + } +} diff --git a/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java b/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java index 8af75d2..e83d749 100644 --- a/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java +++ b/src/main/java/com/favoriteplace/app/service/PilgrimageCommandService.java @@ -7,12 +7,12 @@ import com.favoriteplace.app.domain.enums.PointType; import com.favoriteplace.app.domain.enums.RallyVersion; import com.favoriteplace.app.domain.item.AcquiredItem; -import com.favoriteplace.app.domain.item.PointHistory; import com.favoriteplace.app.domain.travel.*; import com.favoriteplace.app.dto.CommonResponseDto; import com.favoriteplace.app.dto.travel.PilgrimageDto; import com.favoriteplace.app.dto.travel.PilgrimageSocketDto; import com.favoriteplace.app.repository.*; +import com.favoriteplace.app.service.fcm.FCMNotificationService; import com.favoriteplace.global.exception.ErrorCode; import com.favoriteplace.global.exception.RestApiException; import com.favoriteplace.global.websocket.RedisService; @@ -22,15 +22,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.Instant; -import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import static com.favoriteplace.app.service.fcm.FCMNotificationService.makeAnimationTopicName; + @Service @Slf4j @Transactional @@ -45,6 +44,8 @@ public class PilgrimageCommandService { private final PointHistoryRepository pointHistoryRepository; private final CompleteRallyRepository completeRallyRepository; private final AcquiredItemRepository acquiredItemRepository; + private final FCMNotificationService fcmNotificationService; + private final EntityManager em; private final RedisService redisService; private Map> lastButtonStateCache = new ConcurrentHashMap<>(); @@ -75,8 +76,7 @@ public CommonResponseDto.PostResponseDto likeToRally(Long rallyId, Member member * @param member 인증한 사용자 * @return */ - public CommonResponseDto.RallyResponseDto certifyToPilgrimage(Long pilgrimageId, - Member member) { + public CommonResponseDto.RallyResponseDto certifyToPilgrimage(Long pilgrimageId, Member member) { Pilgrimage pilgrimage = pilgrimageRepository.findById(pilgrimageId).orElseThrow( () -> new RestApiException(ErrorCode.PILGRIMAGE_NOT_FOUND)); @@ -140,6 +140,26 @@ private void successVisitedAndPointProcess(Member member, Pilgrimage pilgrimage) log.info("clear"); } + /** + * 랠리 구독 (FCM 토픽에 해당 사용자의 토큰 추가) + */ + public void subscribeRally(Long rallyId, Member member) { + if(member.getFcmToken() == null){ + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + fcmNotificationService.subscribeTopic(makeAnimationTopicName(rallyId), member.getFcmToken()); + } + + /** + * 랠리 구독 취소 (FCM 토픽에 해당 사용자의 토큰 제거) + */ + public void unsubscribeRally(Long rallyId, Member member) { + if (member.getFcmToken() == null) { + throw new RestApiException(ErrorCode.FCM_TOKEN_NOT_FOUND); + } + fcmNotificationService.unsubscribeTopic(makeAnimationTopicName(rallyId), member.getFcmToken()); + } + public boolean isUserAtPilgrimage(Pilgrimage pilgrimage, Double latitude, Double longitude) { return (pilgrimage.getLatitude() + MAX_DISTANCE_WITHIN_100M >= latitude && pilgrimage.getLatitude() - MAX_DISTANCE_WITHIN_100M <= latitude) && (pilgrimage.getLongitude() + MAX_DISTANCE_WITHIN_100M >= longitude && pilgrimage.getLongitude() - MAX_DISTANCE_WITHIN_100M <= longitude); @@ -158,8 +178,6 @@ public PilgrimageSocketDto.ButtonState buttonStatusUpdate(Long pilgrimageId, Pil // 위치 정보 바탕으로 인증 가능 여부 Redis 저장 isLocationVerified(member, pilgrimage, userLocation.getLatitude(), userLocation.getLongitude()); - // 버튼 상태 업데이트 - PilgrimageSocketDto.ButtonState buttonState = determineButtonState(member, pilgrimageId); // 이전 버튼 상태와 비교해서 달라졌다면 전송, 아니면 null synchronized (this) { @@ -173,7 +191,10 @@ public PilgrimageSocketDto.ButtonState buttonStatusUpdate(Long pilgrimageId, Pil PilgrimageSocketDto.ButtonState lastState = pilgrimageStateMap.get(pilgrimageId); - if (lastState == null || !buttonState.equals(lastState)) { + // 버튼 상태 업데이트 + PilgrimageSocketDto.ButtonState buttonState = determineButtonState(member, pilgrimageId); + + if (!buttonState.equals(lastState)) { pilgrimageStateMap.put(pilgrimageId, buttonState); return buttonState; } @@ -200,6 +221,12 @@ public void isLocationVerified(Member member, Pilgrimage pilgrimage, Double lati * @return */ public PilgrimageSocketDto.ButtonState determineButtonState(Member member, Long pilgrimageId) { + PilgrimageSocketDto.ButtonState newState = new PilgrimageSocketDto.ButtonState(); + newState.setCertifyButtonEnabled(false); + newState.setGuestbookButtonEnabled(false); + newState.setMultiGuestbookButtonEnabled(false); + + // 캐시에 저장된 버튼이 없다면 새로 상태 저장 Pilgrimage pilgrimage = pilgrimageRepository.findById(pilgrimageId) .orElseThrow(()->new RestApiException(ErrorCode.PILGRIMAGE_NOT_FOUND)); @@ -208,11 +235,6 @@ public PilgrimageSocketDto.ButtonState determineButtonState(Member member, Long // 사용자가 이번 인증하기에 이미 방명록을 작성했는지 확인 (모든 상호작용 완료했는지) boolean hasWrittenGuestbook = checkIfGuestbookWritten(member, pilgrimage); - PilgrimageSocketDto.ButtonState newState = new PilgrimageSocketDto.ButtonState(); - newState.setCertifyButtonEnabled(false); - newState.setGuestbookButtonEnabled(false); - newState.setMultiGuestbookButtonEnabled(false); - // 24시간 내 인증 기록이 있는가? if (!certifiedInLast) { boolean isCertificationExpired = redisService.isCertificationExpired(member, pilgrimage); @@ -224,10 +246,37 @@ else if (certifiedInLast && !hasWrittenGuestbook) { newState.setGuestbookButtonEnabled(hasMultiWrittenGuestbook? false : true); newState.setMultiGuestbookButtonEnabled(hasMultiWrittenGuestbook? true : false); } - lastButtonStateCache.get(member.getId()).put(pilgrimageId, newState); + synchronized (this) { + lastButtonStateCache.get(member.getId()).put(pilgrimageId, newState); + } return newState; } + /** + * + * @return + */ + public PilgrimageSocketDto.ButtonState initButton (Member member, Long pilgrimageId) { + PilgrimageSocketDto.ButtonState newState = new PilgrimageSocketDto.ButtonState(); + newState.setCertifyButtonEnabled(false); + newState.setGuestbookButtonEnabled(false); + newState.setMultiGuestbookButtonEnabled(false); + + // 이미 캐시에 저장된 버튼이 있다면 바로 호출 + synchronized (this) { + lastButtonStateCache.putIfAbsent(member.getId(), new ConcurrentHashMap<>()); + Map pilgrimageStateMap = lastButtonStateCache.get(member.getId()); + + PilgrimageSocketDto.ButtonState lastState = pilgrimageStateMap.get(pilgrimageId); + + if (lastState != null) { + pilgrimageStateMap.put(pilgrimageId, newState); + return lastState; + } + return newState; + } + } + /** * 24시간 이내 인증 기록이 있는지 확인 * @param member diff --git a/src/main/java/com/favoriteplace/app/service/community/CommentCommandService.java b/src/main/java/com/favoriteplace/app/service/community/CommentCommandService.java index a5b1c0e..0737fff 100644 --- a/src/main/java/com/favoriteplace/app/service/community/CommentCommandService.java +++ b/src/main/java/com/favoriteplace/app/service/community/CommentCommandService.java @@ -1,6 +1,7 @@ package com.favoriteplace.app.service.community; import com.favoriteplace.app.domain.Member; +import com.favoriteplace.app.domain.Notification; import com.favoriteplace.app.domain.community.Comment; import com.favoriteplace.app.domain.community.GuestBook; import com.favoriteplace.app.domain.community.Post; @@ -8,7 +9,9 @@ import com.favoriteplace.app.dto.community.CommentRequestDto; import com.favoriteplace.app.repository.CommentRepository; import com.favoriteplace.app.repository.GuestBookRepository; +import com.favoriteplace.app.repository.NotificationRepository; import com.favoriteplace.app.repository.PostRepository; +import com.favoriteplace.app.service.fcm.FCMNotificationService; import com.favoriteplace.global.exception.ErrorCode; import com.favoriteplace.global.exception.RestApiException; import lombok.RequiredArgsConstructor; @@ -17,33 +20,94 @@ import java.util.Optional; +import static com.favoriteplace.app.converter.FcmConverter.*; +import static com.favoriteplace.app.converter.NotificationConverter.*; + @Service @RequiredArgsConstructor public class CommentCommandService { private final PostRepository postRepository; private final GuestBookRepository guestBookRepository; private final CommentRepository commentRepository; + private final NotificationRepository notificationRepository; + private final FCMNotificationService fcmNotificationService; /** * 자유게시글 새로운 댓글 작성 */ @Transactional - public void createPostComment(Member member, long postId, CommentRequestDto.CreateComment dto) { + public Long createPostComment(Member member, long postId, CommentRequestDto.CreateComment dto) { Post post = postRepository.findById(postId).orElseThrow(() -> new RestApiException(ErrorCode.POST_NOT_FOUND)); Comment newComment = setCommentRelation(member, dto); post.addComment(newComment); commentRepository.save(newComment); + return newComment.getId(); } /** * 성지순례 인증글에 댓글 추가 */ @Transactional - public void createGuestBookComment(Member member, Long guestbookId, CommentRequestDto.CreateComment dto) { + public Long createGuestBookComment(Member member, Long guestbookId, CommentRequestDto.CreateComment dto) { GuestBook guestBook = guestBookRepository.findById(guestbookId).orElseThrow(() -> new RestApiException(ErrorCode.GUESTBOOK_NOT_FOUND)); Comment newComment = setCommentRelation(member, dto); guestBook.addComment(newComment); commentRepository.save(newComment); + return newComment.getId(); + } + + /** + * 자유 게시판 관련 알림 전송 + */ + @Transactional + public void sendPostNotification(Long postId, Long commentId){ + Post post = postRepository.findById(postId).orElseThrow(() -> new RestApiException(ErrorCode.POST_NOT_FOUND)); + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new RestApiException(ErrorCode.COMMENT_NOT_FOUND)); + + // 게시글 작성자에게 전송 + fcmNotificationService.sendNotificationByToken(toPostWriter(post, comment)); + Notification postNotification = toPostNewComment(post, comment); + notificationRepository.save(postNotification); + + // 댓글에 언급된 사람에게 전송 + if(comment.getParentComment() != null){ // 부모 댓글에게 전송 + Notification commentNotification; + if(comment.getReferenceComment() == null){ + fcmNotificationService.sendNotificationByToken(toParentCommentWriter(post, comment)); + commentNotification = toPostParentNewSubComment(post, comment); + }else{ // reference 댓글에게 전송 + fcmNotificationService.sendNotificationByToken(toReferCommentWriter(post, comment)); + commentNotification = toPostReferNewSubComment(post, comment); + } + notificationRepository.save(commentNotification); + } + } + + /** + * 성지 순례 인증글 관련 알림 전송 + */ + @Transactional + public void sendGuestBookNotification(Long guestBookId, Long commentId){ + GuestBook guestBook = guestBookRepository.findById(guestBookId).orElseThrow(() -> new RestApiException(ErrorCode.GUESTBOOK_NOT_FOUND)); + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new RestApiException(ErrorCode.COMMENT_NOT_FOUND)); + + // 게시글 작성자에게 전송 + fcmNotificationService.sendNotificationByToken(toGuestBookWriter(guestBook, comment)); + Notification postNotification = toGuestBookNewComment(guestBook, comment); + notificationRepository.save(postNotification); + + // 댓글에 언급된 사람에게 전송 + if(comment.getParentComment() != null){ + Notification commentNotification; + if(comment.getReferenceComment() == null){ // 부모 댓글에게 전송 + fcmNotificationService.sendNotificationByToken(toParentCommentWriter(guestBook, comment)); + commentNotification = toGuestBookParentNewSubComment(guestBook, comment); + }else{ // reference 댓글에게 전송 + fcmNotificationService.sendNotificationByToken(toReferCommentWriter(guestBook, comment)); + commentNotification = toGuestBookReferNewSubComment(guestBook, comment); + } + notificationRepository.save(commentNotification); + } } /** diff --git a/src/main/java/com/favoriteplace/app/service/fcm/FCMNotificationService.java b/src/main/java/com/favoriteplace/app/service/fcm/FCMNotificationService.java new file mode 100644 index 0000000..d17e79b --- /dev/null +++ b/src/main/java/com/favoriteplace/app/service/fcm/FCMNotificationService.java @@ -0,0 +1,174 @@ +package com.favoriteplace.app.service.fcm; + +import com.favoriteplace.app.domain.Member; +import com.favoriteplace.app.repository.LikedRallyRepository; +import com.favoriteplace.app.service.fcm.dto.PostTokenCond; +import com.favoriteplace.app.service.fcm.enums.TotalTopicMessage; +import com.favoriteplace.global.exception.ErrorCode; +import com.favoriteplace.global.exception.RestApiException; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.TopicManagementResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class FCMNotificationService { + private final FirebaseMessaging firebaseMessaging; + private final LikedRallyRepository likedRallyRepository; + + /** + * token - 단일 기기 + */ + @Transactional + public String sendNotificationByToken(PostTokenCond postTokenCond) { + try{ + Message message = makeTokenMessage(postTokenCond); + return firebaseMessaging.send(message); + } catch (FirebaseMessagingException e) { + log.warn("fcm: {}", e.getErrorCode()); + log.warn("fcm : {}", e.getMessage()); + throw new RestApiException(ErrorCode.TOKEN_ALARM_NOT_SEND); + } + } + + /** + * token - Message 제작 + */ + private Message makeTokenMessage(PostTokenCond postTokenCond){ + return Message.builder() + .setToken(postTokenCond.token()) + .putData("type", postTokenCond.tokenMessage().getType()) + .putData("title", postTokenCond.tokenMessage().getTitle()) + .putData("message", postTokenCond.message()) + .putData("postId", postTokenCond.postId() != null ? postTokenCond.postId().toString() : null) + .putData("guestBookId", postTokenCond.guestBookId() != null ? postTokenCond.guestBookId().toString() : null) + .putData("notificationId", postTokenCond.notificationId().toString()) + .build(); + } + + /** + * topic 구독 + */ + @Transactional + public void subscribeTopic(String topic, String token){ + try{ + List tokens = Collections.singletonList(token); + TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic(tokens, topic); + } catch (FirebaseMessagingException e){ + throw new RestApiException(ErrorCode.TOPIC_SUBSCRIBE_FAIL); + } + } + + /** + * topic 구독 취소 + */ + @Transactional + public void unsubscribeTopic(String topic, String token){ + try{ + List tokens = Collections.singletonList(token); + TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic(tokens, topic); + + } catch (FirebaseMessagingException e){ + throw new RestApiException(ErrorCode.TOPIC_UNSUBSCRIBE_FAIL); + } + } + + /** + * topic 전송 - 애니메이션 + */ + @Transactional + public String sendAnimationAlarmByTopic(Long animationId, String name) { + try{ + Message message = makeAnimationTopicMessage(animationId, name, 1L); + return firebaseMessaging.send(message); + } catch (FirebaseMessagingException e) { + log.warn("fcm: {}", e.getErrorCode()); + log.warn("fcm : {}", e.getMessage()); + throw new RestApiException(ErrorCode.TOPIC_ALARM_NOT_SEND); + } + } + + /** + * topic 전송 - 전체 알림 + */ + @Transactional + public String sendTotalAlarmByTopic(TotalTopicMessage totalTopicMessage) { + try{ + Message message = makeTotalTopicMessage(totalTopicMessage); + return firebaseMessaging.send(message); + } catch (FirebaseMessagingException e) { + log.warn("fcm: {}", e.getErrorCode()); + log.warn("fcm : {}", e.getMessage()); + throw new RestApiException(ErrorCode.TOPIC_ALARM_NOT_SEND); + } + } + + /** + * topic - 애니메이션 Message 제작 + */ + private Message makeAnimationTopicMessage(Long animationId, String name, Long notificationId){ + Message message = Message.builder() + .setTopic(makeAnimationTopicName(animationId)) + .putData("type", "animation") + .putData("title", String.format("애니메이션 %s의 성지순례가 추가되었습니다!", name)) + .putData("message", "지금 확인하러 가기") + .putData("animationId", animationId.toString()) + .putData("notificationId", notificationId.toString()) + .build(); + return message; + } + + /** + * topic - 홈화면 Message 제작 + */ + private Message makeTotalTopicMessage(TotalTopicMessage totalTopicMessage){ + Message message = Message.builder() + .setTopic("total") + .putData("type", totalTopicMessage.getType()) + .putData("title", totalTopicMessage.getTitle()) + .putData("message", totalTopicMessage.getMessage()) + .build(); + return message; + } + + public static String makeAnimationTopicName(Long animationId){ + return "animation" + animationId.toString(); + } + + /** + * topic - 로그인 시 FCM 처리 + * a. 신규 회원일 경우(old FCM token 존재 X) - 추가로 해야할 작업 없음 + * b. 신규 회원이 아닌데, 기기 변경이 없는 경우 (old FCM == new FCM) - 추가로 해야할 작업 없음 + * c. 신규 회원이 아닌데, 기기 변경이 있는 경우 (old FCM != new FCM) + * c-1. 전체 사용자 알림 : 전체 topic 에서 old FCM 구독 해제 & new FCM 구독 + * c-2. 애니메이션 개별 알림 : 사용자가 좋아요한 애니메이션들의 PK다 가져옴 -> old FCM 구독 해제 & new FCM 구독 + * 이 행위가 다 끝나고 DB에서 FCM 필드 교체 + */ + @Transactional + public void refreshFCMTopicAndToken(Member member, String newFcm){ + String oldFcm = member.getFcmToken(); + if(oldFcm != null && !oldFcm.equals(newFcm)){ + // 전체 사용자 알림 + unsubscribeTopic("total", oldFcm); + subscribeTopic("total", newFcm); + // 애니메이션 개별 알림 + List likedAnimations = likedRallyRepository.findDistinctRallyIdsByMember(member.getId()); + for(Long id:likedAnimations){ + String topic = makeAnimationTopicName(id); + unsubscribeTopic(topic, oldFcm); + subscribeTopic(topic, newFcm); + } + } + member.refreshFcmToken(newFcm); + } + +} diff --git a/src/main/java/com/favoriteplace/app/service/fcm/dto/PostTokenCond.java b/src/main/java/com/favoriteplace/app/service/fcm/dto/PostTokenCond.java new file mode 100644 index 0000000..df17c90 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/service/fcm/dto/PostTokenCond.java @@ -0,0 +1,15 @@ +package com.favoriteplace.app.service.fcm.dto; + +import com.favoriteplace.app.service.fcm.enums.TokenMessage; +import lombok.Builder; + +@Builder +public record PostTokenCond( + String token, + TokenMessage tokenMessage, + Long postId, + Long guestBookId, + String message, + Long notificationId +) { +} diff --git a/src/main/java/com/favoriteplace/app/service/fcm/enums/TokenMessage.java b/src/main/java/com/favoriteplace/app/service/fcm/enums/TokenMessage.java new file mode 100644 index 0000000..aebb572 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/service/fcm/enums/TokenMessage.java @@ -0,0 +1,17 @@ +package com.favoriteplace.app.service.fcm.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TokenMessage { + POST_NEW_COMMENT("post", "내 글에 새로운 댓글이 달렸어요!"), + GUESTBOOK_NEW_COMMENT("guestBook", "내 인증글에 새로운 댓글이 달렸어요!"), + POST_COMMENT_NEW_SUBCOMMENT("post", "내 댓글에 새로운 답글이 달렸어요!"), + GUESTBOOK_COMMENT_NEW_SUBCOMMENT("guestBook", "내 댓글에 새로운 답글이 달렸어요!") + ; + + private final String type; + private final String title; +} diff --git a/src/main/java/com/favoriteplace/app/service/fcm/enums/TotalTopicMessage.java b/src/main/java/com/favoriteplace/app/service/fcm/enums/TotalTopicMessage.java new file mode 100644 index 0000000..7a81be6 --- /dev/null +++ b/src/main/java/com/favoriteplace/app/service/fcm/enums/TotalTopicMessage.java @@ -0,0 +1,15 @@ +package com.favoriteplace.app.service.fcm.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TotalTopicMessage { + INFORM("home", "새로운 기능이 업데이트 되었어요!", "새로운 기능을 확인해보세요!") + ; + + private final String type; + private final String title; + private final String message; +} diff --git a/src/main/java/com/favoriteplace/global/config/FcmConfig.java b/src/main/java/com/favoriteplace/global/config/FcmConfig.java new file mode 100644 index 0000000..f292a2e --- /dev/null +++ b/src/main/java/com/favoriteplace/global/config/FcmConfig.java @@ -0,0 +1,84 @@ +package com.favoriteplace.global.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +@Configuration +public class FcmConfig { + @Value("${fcm.type}") + private String type; + + @Value("${fcm.project_id}") + private String projectId; + + @Value("${fcm.private_key_id}") + private String privateKeyId; + + @Value("${fcm.private_key}") + private String privateKey; + + @Value("${fcm.client_email}") + private String clientEmail; + + @Value("${fcm.client_id}") + private String clientId; + + @Value("${fcm.auth_uri}") + private String authUri; + + @Value("${fcm.token_uri}") + private String tokenUri; + + @Value("${fcm.auth_provider_x509_cert_url}") + private String authProviderX509CertUrl; + + @Value("${fcm.client_x509_cert_url}") + private String clientX509CertUrl; + + @Value("${fcm.universe_domain}") + private String universeDomain; + + @Bean + public FirebaseApp firebaseApp() throws IOException{ + String jsonKey = String.format( + "{" + + "\"type\":\"%s\"," + + "\"project_id\":\"%s\"," + + "\"private_key_id\":\"%s\"," + + "\"private_key\":\"%s\"," + + "\"client_email\":\"%s\"," + + "\"client_id\":\"%s\"," + + "\"auth_uri\":\"%s\"," + + "\"token_uri\":\"%s\"," + + "\"auth_provider_x509_cert_url\":\"%s\"," + + "\"client_x509_cert_url\":\"%s\"," + + "\"universe_domain\":\"%s\"" + + "}", + type, projectId, privateKeyId, privateKey, clientEmail, clientId, + authUri, tokenUri, authProviderX509CertUrl, clientX509CertUrl, universeDomain + ); + + GoogleCredentials credentials = GoogleCredentials.fromStream( + new ByteArrayInputStream(jsonKey.getBytes()) + ); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(credentials) + .build(); + + return FirebaseApp.initializeApp(options); + } + + @Bean + public FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp){ + return FirebaseMessaging.getInstance(firebaseApp); + } +} diff --git a/src/main/java/com/favoriteplace/global/exception/ErrorCode.java b/src/main/java/com/favoriteplace/global/exception/ErrorCode.java index aee1137..ed8ced4 100644 --- a/src/main/java/com/favoriteplace/global/exception/ErrorCode.java +++ b/src/main/java/com/favoriteplace/global/exception/ErrorCode.java @@ -26,6 +26,7 @@ public enum ErrorCode { USER_NOT_AUTHOR(HttpStatus.FORBIDDEN, 2005, "해당 게시글의 작성자가 아닙니다."), CANT_BLOCK_SELF(HttpStatus.FORBIDDEN, 2006, "스스로를 차단할 수 없습니다."), TOKEN_NOT_VALID(HttpStatus.BAD_REQUEST, 2007, "not valid token"), + FCM_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, 2008, "사용자의 FCM Token이 존재하지 않습니다."), NOT_SIGNUP_WITH_KAKAO(HttpStatus.BAD_REQUEST, 2008, "해당 계정으로 회원가입한 이력이 없습니다. 카카오 회원가입 필요."), @@ -68,7 +69,16 @@ public enum ErrorCode { IMAGE_FORMAT_ERROR(HttpStatus.BAD_REQUEST, 11001, "올바른 이미지 파일이 아닙니다."), IMAGE_NOT_READABLE(HttpStatus.BAD_REQUEST, 11002, "이미지 파일을 읽을 수 없습니다."), IMAGE_CANNOT_UPLOAD(HttpStatus.BAD_REQUEST, 11003, "이미지 파일을 업로드할 수 없습니다."), - IMAGE_SIZE_TOO_BIG(HttpStatus.PAYLOAD_TOO_LARGE, 11004, "각각의 이미지 파일의 사이즈가 4MB를 넘어갈 수 없습니다."); + IMAGE_SIZE_TOO_BIG(HttpStatus.PAYLOAD_TOO_LARGE, 11004, "각각의 이미지 파일의 사이즈가 4MB를 넘어갈 수 없습니다."), + + //알림 (12000번대) + TOKEN_ALARM_NOT_SEND(HttpStatus.BAD_REQUEST, 120001, "[token] push 알림이 전송되지 않았습니다."), + TOPIC_ALARM_NOT_SEND(HttpStatus.BAD_REQUEST, 120002, "[topic] push 알림이 전송되지 않았습니다."), + TOPIC_SUBSCRIBE_FAIL(HttpStatus.BAD_REQUEST, 12003, "[topic] 구독에 실패했습니다."), + TOPIC_UNSUBSCRIBE_FAIL(HttpStatus.BAD_REQUEST, 12004, "[topic] 구독 취소에 실패했습니다."), + NOTIFICATION_NOT_EXIST(HttpStatus.NOT_FOUND, 12005, "해당 알림이 존재하지 않습니다."), + NOTIFICATION_NOT_BELONG(HttpStatus.CONFLICT, 12006, "해당 사용자의 알림이 아닙니다.") + ; private final HttpStatus httpStatus; private final int code; diff --git a/src/main/java/com/favoriteplace/global/security/config/SecurityConfig.java b/src/main/java/com/favoriteplace/global/security/config/SecurityConfig.java index 342eb46..8f217d7 100644 --- a/src/main/java/com/favoriteplace/global/security/config/SecurityConfig.java +++ b/src/main/java/com/favoriteplace/global/security/config/SecurityConfig.java @@ -1,9 +1,9 @@ package com.favoriteplace.global.security.config; -import com.favoriteplace.global.security.Filter.ExceptionHandlerFilter; -import com.favoriteplace.global.security.Filter.JwtAuthenticationEntryPoint; -import com.favoriteplace.global.security.Filter.JwtAuthenticationFilter; -import com.favoriteplace.global.security.Filter.LoginFilter; +import com.favoriteplace.global.security.filter.ExceptionHandlerFilter; +import com.favoriteplace.global.security.filter.JwtAuthenticationEntryPoint; +import com.favoriteplace.global.security.filter.JwtAuthenticationFilter; +import com.favoriteplace.global.security.filter.LoginFilter; import com.favoriteplace.global.security.handler.CustomAuthenticationFailHandler; import com.favoriteplace.global.security.handler.CustomAuthenticationSuccessHandler; import com.favoriteplace.global.security.handler.JwtAccessDeniedHandler; @@ -12,8 +12,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -22,7 +20,6 @@ import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; diff --git a/src/main/java/com/favoriteplace/global/security/Filter/ExceptionHandlerFilter.java b/src/main/java/com/favoriteplace/global/security/filter/ExceptionHandlerFilter.java similarity index 97% rename from src/main/java/com/favoriteplace/global/security/Filter/ExceptionHandlerFilter.java rename to src/main/java/com/favoriteplace/global/security/filter/ExceptionHandlerFilter.java index 6ad78de..96bf0a3 100644 --- a/src/main/java/com/favoriteplace/global/security/Filter/ExceptionHandlerFilter.java +++ b/src/main/java/com/favoriteplace/global/security/filter/ExceptionHandlerFilter.java @@ -1,4 +1,4 @@ -package com.favoriteplace.global.security.Filter; +package com.favoriteplace.global.security.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.favoriteplace.global.exception.ErrorCode; diff --git a/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationEntryPoint.java b/src/main/java/com/favoriteplace/global/security/filter/JwtAuthenticationEntryPoint.java similarity index 94% rename from src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationEntryPoint.java rename to src/main/java/com/favoriteplace/global/security/filter/JwtAuthenticationEntryPoint.java index cb09724..aa1aa0b 100644 --- a/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/favoriteplace/global/security/filter/JwtAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package com.favoriteplace.global.security.Filter; +package com.favoriteplace.global.security.filter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationFilter.java b/src/main/java/com/favoriteplace/global/security/filter/JwtAuthenticationFilter.java similarity index 93% rename from src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationFilter.java rename to src/main/java/com/favoriteplace/global/security/filter/JwtAuthenticationFilter.java index 38a64c3..ebf2d56 100644 --- a/src/main/java/com/favoriteplace/global/security/Filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/favoriteplace/global/security/filter/JwtAuthenticationFilter.java @@ -1,4 +1,4 @@ -package com.favoriteplace.global.security.Filter; +package com.favoriteplace.global.security.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.favoriteplace.global.exception.ErrorCode; @@ -15,7 +15,6 @@ import java.util.List; import java.util.regex.Pattern; -import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -35,6 +34,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { //인증이 필수인 경우 추가 new ExcludePath("/auth/logout", HttpMethod.POST), new ExcludePath("/pilgrimage/**", HttpMethod.POST), + new ExcludePath("/pilgrimage/**", HttpMethod.DELETE), new ExcludePath("/posts/free/my-posts", HttpMethod.GET), new ExcludePath("/posts/free/my-comments", HttpMethod.GET), new ExcludePath("/posts/free", HttpMethod.POST), @@ -47,6 +47,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { new ExcludePath("/my", HttpMethod.GET), new ExcludePath("/my/**", HttpMethod.GET), new ExcludePath("/my/**", HttpMethod.PUT), + new ExcludePath("/my/**", HttpMethod.PATCH), new ExcludePath("/posts/guestbooks/**", HttpMethod.PATCH), new ExcludePath("/posts/guestbooks/**", HttpMethod.DELETE), new ExcludePath("/posts/guestbooks/**", HttpMethod.POST), @@ -55,7 +56,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { new ExcludePath("/posts/free/comments/**", HttpMethod.DELETE), new ExcludePath("/posts/guestbooks/comments/**", HttpMethod.PUT), new ExcludePath("/posts/guestbooks/comments/**", HttpMethod.DELETE), - new ExcludePath("/shop/purchase/**", HttpMethod.POST) + new ExcludePath("/shop/purchase/**", HttpMethod.POST), + new ExcludePath("/notifications", HttpMethod.PATCH), + new ExcludePath("/notifications", HttpMethod.GET), + new ExcludePath("/notifications/**", HttpMethod.PATCH), + new ExcludePath("/notifications/**", HttpMethod.DELETE) // Add more paths and methods as needed ); diff --git a/src/main/java/com/favoriteplace/global/security/Filter/LoginFilter.java b/src/main/java/com/favoriteplace/global/security/filter/LoginFilter.java similarity index 93% rename from src/main/java/com/favoriteplace/global/security/Filter/LoginFilter.java rename to src/main/java/com/favoriteplace/global/security/filter/LoginFilter.java index 1362493..4a8a600 100644 --- a/src/main/java/com/favoriteplace/global/security/Filter/LoginFilter.java +++ b/src/main/java/com/favoriteplace/global/security/filter/LoginFilter.java @@ -1,6 +1,5 @@ -package com.favoriteplace.global.security.Filter; +package com.favoriteplace.global.security.filter; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/favoriteplace/global/websocket/RedisService.java b/src/main/java/com/favoriteplace/global/websocket/RedisService.java index e773ed1..1e66a43 100644 --- a/src/main/java/com/favoriteplace/global/websocket/RedisService.java +++ b/src/main/java/com/favoriteplace/global/websocket/RedisService.java @@ -23,8 +23,8 @@ public class RedisService { public void saveCertificationTime(Long userId, Long pilgrimageId) { String key = CERTIFICATION_KEY_PREFIX + userId + ":" + pilgrimageId; String now = DateTimeFormatter.ISO_INSTANT.format(Instant.now()); - redisTemplate.opsForValue().set(key, now); - redisTemplate.expire(key, CERTIFICATION_EXPIRATION); + + redisTemplate.opsForValue().set(key, now, CERTIFICATION_EXPIRATION); } // 인증 시점에서 1분이 지났는지 확인