Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
a17d943
[CHORE] firebase-admin 의존성 추가
Kim-TaeUk Jan 6, 2025
a1963af
[FEAT] FirebaseMessaging bean 등록을 위한 FCMConfig 추가
Kim-TaeUk Jan 13, 2025
aa387cf
[FEAT] Firebase SDK 인증 설정을 위한 GoogleCredentials 생성
Kim-TaeUk Jan 13, 2025
9135d7e
[FEAT] Firebase SDK 초기화를 위한 FirebaseOptions 추가
Kim-TaeUk Jan 13, 2025
ad89ffa
[FEAT] Firebase SDK 초기화를 위한 FirebaseOptions 추가
Kim-TaeUk Jan 13, 2025
eac6dd4
[FEAT] Spring Bean으로 FirebaseMessaging 등록
Kim-TaeUk Jan 13, 2025
3a130ac
[REFACTOR] Firebase 설정 경로 @Value를 통해 외부에서 주입하도록 수정
Kim-TaeUk Jan 13, 2025
c5440d4
[FEAT] FCM message 전송을 위한 FCMService 추가
Kim-TaeUk Jan 13, 2025
8460aa8
[CHORE] FirebaseMessaging Bean 의존성 주입
Kim-TaeUk Jan 13, 2025
a5cbdb1
[CHORE] FCM key file gitignore에 추가
Kim-TaeUk Jan 15, 2025
330334c
[FEAT] Message에 필요한 AndroidConfig 추가
Kim-TaeUk Jan 15, 2025
a19323a
[FEAT] Message에 필요한 ApsConfig 추가
Kim-TaeUk Jan 15, 2025
c23cfc7
[FEAT] specific device에 대한 Message 생성 로직 추가
Kim-TaeUk Jan 15, 2025
3e4710c
[FEAT] FCM에 specific device에 대한 Message 전송 로직 추가
Kim-TaeUk Jan 15, 2025
6286ced
[CHORE] logging을 위한 Slf4j 추가
Kim-TaeUk Jan 15, 2025
4a19453
[FEAT] firebaseMessaging.send()에서 발생하는 Exception 처리
Kim-TaeUk Jan 15, 2025
c93d9b4
[COMMENT] FirebaseMessagingException logging 외 추가 처리 작업 가능성 코멘트 추가
Kim-TaeUk Jan 15, 2025
db4b1f3
[REFACTOR] Message 생성 로직 method로 분리
Kim-TaeUk Jan 15, 2025
97de844
[REFACTOR] Message 생성부 try문 밖으로 분리
Kim-TaeUk Jan 15, 2025
c77a256
[CHORE] logging을 위한 Slf4j 추가
Kim-TaeUk Jan 15, 2025
1658b73
[FIX] FirebaseMessaging Bean 등록 실패 시, 예외 throw 및 logging 추가
Kim-TaeUk Jan 15, 2025
43bf246
[FEAT] 푸시 알림 지원을 위한 FCM token 컬럼 User 엔티티에 추가
Kim-TaeUk Jan 15, 2025
9584af6
[FEAT] FCM token 등록 요청 처리를 위한 FCMTokenRequest DTO 추가
Kim-TaeUk Jan 15, 2025
bd0a14a
[FEAT] UserController에 FCM token 등록 엔드포인트 추가
Kim-TaeUk Jan 15, 2025
b2b108c
[FEAT] User 엔티티에 FCM token 업데이트 메서드 추가
Kim-TaeUk Jan 15, 2025
41d366a
[FEAT] UserService에 FCM token 등록 로직 추가
Kim-TaeUk Jan 15, 2025
bf1fffc
[FEAT] FCMTokenRequest에 @NotBlank 유효성 검사를 추가
Kim-TaeUk Jan 15, 2025
97542c4
[FIX] 수정된 데이터 포맷에 맞게 수정
Kim-TaeUk Jan 19, 2025
fe18600
[CHORE] FCMService Bean 의존성 주입
Kim-TaeUk Jan 19, 2025
b674512
[FEAT] feed 작성자(알림 보낼 대상)의 fcm token 획득
Kim-TaeUk Jan 19, 2025
7cb57cb
[FEAT] 연결 작품이 있는 경우와 없는 경우로 로직 분리
Kim-TaeUk Jan 19, 2025
0f8f705
[FEAT] 연결 작품이 없는 경우 sendPushMessage 로직 추가
Kim-TaeUk Jan 19, 2025
b089751
[FEAT] 연결 작품이 있는 경우 sendPushMessage 로직 추가
Kim-TaeUk Jan 19, 2025
9806c62
[REFACTOR] 좋아요 발생 시 push 전송 로직 메서드로 분리
Kim-TaeUk Jan 19, 2025
b9723a9
[REFACTOR] 연결 작품에 따른 title 설정 로직 메서드로 분리
Kim-TaeUk Jan 19, 2025
dfad14f
[FIX] 정책에 맞게 title 길이 조건 반영
Kim-TaeUk Jan 19, 2025
40129f1
[REFACTOR] sendPushMessage()의 파라미터 inline
Kim-TaeUk Jan 19, 2025
1d6b803
[CHORE] FCMService Bean 의존성 주입
Kim-TaeUk Jan 19, 2025
77000ec
[CHORE] NovelService Bean 의존성 주입
Kim-TaeUk Jan 19, 2025
6ba337f
[FEAT] 지금 뜨는 수다글 선정 시 push 전송 로직 추가
Kim-TaeUk Jan 19, 2025
4d34e2f
[REFACTOR] 작품 연결 여부에 따라 달라지는 notification body 생섬 부분 메서드로 분리
Kim-TaeUk Jan 19, 2025
c12aa73
[FIX] 정책에 맞게 UX라이팅 수정
Kim-TaeUk Jan 19, 2025
bd68395
[CHORE] FCMService Bean 의존성 주입
Kim-TaeUk Jan 19, 2025
486f3fb
[FIX] CommentService의 createComment() 메서드 시그니처 변경
Kim-TaeUk Jan 19, 2025
e103d45
[CHORE] NovelService Bean 의존성 주입
Kim-TaeUk Jan 19, 2025
5830760
[FEAT] 내 글에 댓글 남겼을 때 sendPushMessage 로직 추가
Kim-TaeUk Jan 19, 2025
b8e657c
[REMOVE] 불필요한 주석 제거
Kim-TaeUk Jan 19, 2025
f1d7585
[RENAME] 내 글에 댓글 남겼을 때 푸시 발송 메서드 rename
Kim-TaeUk Jan 19, 2025
2f30455
[FEAT] multiple devices에 대한 Message 생성 로직 추가 - Multicast Message
Kim-TaeUk Jan 19, 2025
f9c1136
[FEAT] firebaseMessaging.sendMulticast()에서 발생하는 Exception 처리
Kim-TaeUk Jan 19, 2025
d137357
[FEAT] 내가 댓글 단 글에 새로운 댓글이 달린 경우 sendMulticastPushMessage 로직 추가
Kim-TaeUk Jan 19, 2025
ea961fa
[RENAME] from sendMulticastMessage to sendMulticastPushMessage
Kim-TaeUk Jan 19, 2025
497b8cc
[FIX] FCM 알림 대상에서 본인은 제외
Kim-TaeUk Jan 19, 2025
9ce4c1a
[FEAT] FCM 메시지 처리를 위한 FCMMessageRequest DTO 추가
Kim-TaeUk Jan 19, 2025
6451275
[REFACTOR] FCM message 처리 시 FCMNotificationRequest DTO로 간소화
Kim-TaeUk Jan 19, 2025
c6e96f6
[FIX] UX 라이팅에서 누락된 이모지 추가
Kim-TaeUk Jan 20, 2025
7605b98
[FIX] 중복 체크를 통한 동일 push 중복 전송 방지
Kim-TaeUk Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ src/main/generated/

## Apple Login Key File ##
*.p8

/src/main/resources/websoso-fcm.json
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ dependencies {

//Apple Login
implementation 'com.nimbusds:nimbus-jose-jwt:3.10'

// FCM
implementation 'com.google.firebase:firebase-admin:8.1.0'
}

tasks.named('test') {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/websoso/WSSServer/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.websoso.WSSServer.dto.feed.UserFeedsGetResponse;
import org.websoso.WSSServer.dto.user.EditMyInfoRequest;
import org.websoso.WSSServer.dto.user.EditProfileStatusRequest;
import org.websoso.WSSServer.dto.user.FCMTokenRequest;
import org.websoso.WSSServer.dto.user.LoginResponse;
import org.websoso.WSSServer.dto.user.MyProfileResponse;
import org.websoso.WSSServer.dto.user.NicknameValidation;
Expand Down Expand Up @@ -209,4 +210,14 @@ public ResponseEntity<UserIdAndNicknameResponse> getUserIdAndNicknameAndGender(P
.status(OK)
.body(userService.getUserIdAndNicknameAndGender(user));
}

@PostMapping("/fcm-token")
public ResponseEntity<Void> registerFCMToken(Principal principal,
@Valid @RequestBody FCMTokenRequest fcmTokenRequest) {
User user = userService.getUserOrException(Long.valueOf(principal.getName()));
userService.registerFCMToken(user, fcmTokenRequest.fcmToken());
return ResponseEntity
.status(OK)
.build();
}
}
7 changes: 7 additions & 0 deletions src/main/java/org/websoso/WSSServer/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public class User {
@Column(nullable = false)
private String socialId;

@Column
private String fcmToken;

@Column(columnDefinition = "varchar(10)", nullable = false)
private String nickname;
//TODO 일부 특수문자 제외, 앞뒤 공백 불가능
Expand Down Expand Up @@ -136,4 +139,8 @@ public void editMyInfo(EditMyInfoRequest editMyInfoRequest) {
this.gender = Gender.valueOf(editMyInfoRequest.gender());
this.birth = Year.of(editMyInfoRequest.birth());
}

public void updateFCMToken(String fcmToken) {
this.fcmToken = fcmToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.websoso.WSSServer.dto.user;

import jakarta.validation.constraints.NotBlank;

public record FCMTokenRequest(
@NotBlank(message = "FCM Token 값은 null 이거나, 공백일 수 없습니다.")
String fcmToken
) {
}
41 changes: 41 additions & 0 deletions src/main/java/org/websoso/WSSServer/notification/FCMConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.websoso.WSSServer.notification;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

@Slf4j
@Configuration
public class FCMConfig {

@Value("${fcm.key-path}")
private String firebaseConfigPath;

@Bean
public FirebaseMessaging firebaseMessaging() {
try {
GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(firebaseConfigPath).getInputStream());

FirebaseOptions firebaseOptions = FirebaseOptions.builder()
.setCredentials(googleCredentials)
.build();

if (FirebaseApp.getApps().isEmpty()) {
FirebaseApp.initializeApp(firebaseOptions);
}

return FirebaseMessaging.getInstance();
} catch (IOException e) {
log.error("[FirebaseMessaging] Failed to initialize FirebaseMessaging", e);
throw new IllegalStateException("Failed to initialize FirebaseMessaging due to firebase key file", e);
}
}
}
83 changes: 83 additions & 0 deletions src/main/java/org/websoso/WSSServer/notification/FCMService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.websoso.WSSServer.notification;

import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.ApsAlert;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.MulticastMessage;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.websoso.WSSServer.notification.dto.FCMMessageRequest;

@Slf4j
@Service
@RequiredArgsConstructor
public class FCMService {

private final FirebaseMessaging firebaseMessaging;

public void sendPushMessage(String targetFCMToken, FCMMessageRequest fcmMessageRequest) {
Message message = createMessage(targetFCMToken, fcmMessageRequest);

try {
firebaseMessaging.send(message);
} catch (FirebaseMessagingException e) {
log.error("[FirebaseMessagingException] exception ", e);
// TODO: discord로 알림 추가 혹은 후속 작업 논의 후 추가
}
}

private Message createMessage(String targetFCMToken, FCMMessageRequest fcmMessageRequest) {
ApnsConfig apnsConfig = ApnsConfig.builder()
.setAps(Aps.builder()
.setAlert(ApsAlert.builder()
.setTitle(fcmMessageRequest.title())
.setBody(fcmMessageRequest.body())
.build())
.build())
.build();

return Message.builder()
.setToken(targetFCMToken)
.putData("title", fcmMessageRequest.title())
.putData("body", fcmMessageRequest.body())
.putData("feedId", fcmMessageRequest.feedId())
.putData("view", fcmMessageRequest.view())
.setApnsConfig(apnsConfig)
.build();
}

public void sendMulticastPushMessage(List<String> targetFCMTokens, FCMMessageRequest fcmMessageRequest) {
MulticastMessage multicastMessage = createMulticastMessage(targetFCMTokens, fcmMessageRequest);
try {
firebaseMessaging.sendMulticast(multicastMessage);
} catch (Exception e) {
log.error("[FirebaseMessagingException] exception ", e);
// TODO: discord로 알림 추가 혹은 후속 작업 논의 후 추가
}
}

private MulticastMessage createMulticastMessage(List<String> targetFCMTokens, FCMMessageRequest fcmMessageRequest) {
ApnsConfig apnsConfig = ApnsConfig.builder()
.setAps(Aps.builder()
.setAlert(ApsAlert.builder()
.setTitle(fcmMessageRequest.title())
.setBody(fcmMessageRequest.body())
.build())
.build())
.build();

return MulticastMessage.builder()
.addAllTokens(targetFCMTokens)
.putData("title", fcmMessageRequest.title())
.putData("body", fcmMessageRequest.body())
.putData("feedId", fcmMessageRequest.feedId())
.putData("view", fcmMessageRequest.view())
.setApnsConfig(apnsConfig)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.websoso.WSSServer.notification.dto;

public record FCMMessageRequest(
String title,
String body,
String feedId,
String view
) {

public static FCMMessageRequest of(String title, String body, String feedId, String view) {
return new FCMMessageRequest(title, body, feedId, view);
}
}
61 changes: 59 additions & 2 deletions src/main/java/org/websoso/WSSServer/service/CommentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
import org.springframework.transaction.annotation.Transactional;
import org.websoso.WSSServer.domain.Comment;
import org.websoso.WSSServer.domain.Feed;
import org.websoso.WSSServer.domain.Novel;
import org.websoso.WSSServer.domain.User;
import org.websoso.WSSServer.domain.common.DiscordWebhookMessage;
import org.websoso.WSSServer.domain.common.ReportedType;
import org.websoso.WSSServer.dto.comment.CommentGetResponse;
import org.websoso.WSSServer.dto.comment.CommentsGetResponse;
import org.websoso.WSSServer.dto.user.UserBasicInfo;
import org.websoso.WSSServer.exception.exception.CustomCommentException;
import org.websoso.WSSServer.notification.FCMService;
import org.websoso.WSSServer.notification.dto.FCMMessageRequest;
import org.websoso.WSSServer.repository.CommentRepository;

@Service
Expand All @@ -35,9 +38,63 @@ public class CommentService {
private final BlockService blockService;
private final ReportedCommentService reportedCommentService;
private final MessageService messageService;
private final FCMService fcmService;
private final NovelService novelService;

public void createComment(Long userId, Feed feed, String commentContent) {
commentRepository.save(Comment.create(userId, feed, commentContent));
public void createComment(User user, Feed feed, String commentContent) {
commentRepository.save(Comment.create(user.getUserId(), feed, commentContent));
sendCommentPushMessageToFeedOwner(user, feed);
sendCommentPushMessageToCommenters(user, feed);
}

private void sendCommentPushMessageToFeedOwner(User user, Feed feed) {
if (isUserCommentOwner(user, feed.getUser())) {
return;
}

FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of(
createNotificationTitle(feed),
String.format("%s님이 내 수다글에 댓글을 남겼어요", user.getNickname()),
String.valueOf(feed.getFeedId()),
"feedDetail"
);
fcmService.sendPushMessage(
feed.getUser().getFcmToken(),
fcmMessageRequest
);
}

private String createNotificationTitle(Feed feed) {
if (feed.getNovelId() == null) {
String feedContent = feed.getFeedContent();
return feedContent.length() <= 12
? feedContent
: "'" + feedContent.substring(0, 12) + "...'";
}
Novel novel = novelService.getNovelOrException(feed.getNovelId());
return novel.getTitle();
}

private void sendCommentPushMessageToCommenters(User user, Feed feed) {
List<String> commentersUserId = feed.getComments()
.stream()
.map(Comment::getUserId)
.filter(userId -> !userId.equals(user.getUserId()))
.distinct()
.map(userService::getUserOrException)
.map(User::getFcmToken)
.toList();

FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of(
createNotificationTitle(feed),
"내가 댓글 단 수다글에 또 다른 댓글이 달렸어요.",
String.valueOf(feed.getFeedId()),
"feedDetail"
);
fcmService.sendMulticastPushMessage(
commentersUserId,
fcmMessageRequest
);
}

public void updateComment(Long userId, Feed feed, Long commentId, String commentContent) {
Expand Down
35 changes: 34 additions & 1 deletion src/main/java/org/websoso/WSSServer/service/FeedService.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import org.websoso.WSSServer.dto.user.UserBasicInfo;
import org.websoso.WSSServer.exception.exception.CustomFeedException;
import org.websoso.WSSServer.exception.exception.CustomUserException;
import org.websoso.WSSServer.notification.FCMService;
import org.websoso.WSSServer.notification.dto.FCMMessageRequest;
import org.websoso.WSSServer.repository.AvatarRepository;
import org.websoso.WSSServer.repository.FeedRepository;
import org.websoso.WSSServer.repository.NovelRepository;
Expand All @@ -70,6 +72,7 @@ public class FeedService {
private final MessageService messageService;
private final UserService userService;
private final NovelRepository novelRepository;
private final FCMService fcmService;

public void createFeed(User user, FeedCreateRequest request) {
if (request.novelId() != null) {
Expand Down Expand Up @@ -115,6 +118,36 @@ public void likeFeed(User user, Long feedId) {
if (isPopularFeed) {
popularFeedService.createPopularFeed(feed);
}

sendLikePushMessage(user, feed);
}

private void sendLikePushMessage(User liker, Feed feed) {
if (liker.equals(feed.getUser())) {
return;
}

FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of(
createNotificationTitle(feed),
String.format("%s님이 내 수다글을 좋아해요.", liker.getNickname()),
String.valueOf(feed.getFeedId()),
"feedDetail"
);
fcmService.sendPushMessage(
feed.getUser().getFcmToken(),
fcmMessageRequest
);
}

private String createNotificationTitle(Feed feed) {
if (feed.getNovelId() == null) {
String feedContent = feed.getFeedContent();
return feedContent.length() <= 12
? feedContent
: "'" + feedContent.substring(0, 12) + "...'";
}
Novel novel = novelService.getNovelOrException(feed.getNovelId());
return novel.getTitle();
}

public void unLikeFeed(User user, Long feedId) {
Expand Down Expand Up @@ -154,7 +187,7 @@ public FeedsGetResponse getFeeds(User user, String category, Long lastFeedId, in
public void createComment(User user, Long feedId, CommentCreateRequest request) {
Feed feed = getFeedOrException(feedId);
validateFeedAccess(feed, user);
commentService.createComment(user.getUserId(), feed, request.commentContent());
commentService.createComment(user, feed, request.commentContent());
}

public void updateComment(User user, Long feedId, Long commentId, CommentUpdateRequest request) {
Expand Down
Loading