diff --git a/build.gradle b/build.gradle index a46f524..4762e23 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,10 @@ dependencies { implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-mysql") + // FCM + implementation 'com.google.firebase:firebase-admin:9.2.0' + implementation("com.squareup.okhttp3:okhttp") + asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' testAnnotationProcessor 'org.projectlombok:lombok' testImplementation("com.h2database:h2") diff --git a/src/main/java/com/polzzak/domain/notification/dto/FcmMessage.java b/src/main/java/com/polzzak/domain/notification/dto/FcmMessage.java new file mode 100644 index 0000000..bb8113e --- /dev/null +++ b/src/main/java/com/polzzak/domain/notification/dto/FcmMessage.java @@ -0,0 +1,45 @@ +package com.polzzak.domain.notification.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class FcmMessage { + + private boolean validateOnly; + private Message message; + private String to; + + public FcmMessage(boolean validateOnly, Message message, String to) { + this.validateOnly = validateOnly; + this.message = message; + this.to = to; + } + + @NoArgsConstructor + @Getter + public static class Message { + private Notification notification; + private String token; + + public Message(Notification notification, String token) { + this.notification = notification; + this.token = token; + } + } + + @NoArgsConstructor + @Getter + public static class Notification { + private String title; + private String body; + private String image; + + public Notification(String title, String body, String image) { + this.title = title; + this.body = body; + this.image = image; + } + } +} diff --git a/src/main/java/com/polzzak/domain/notification/entity/NotificationType.java b/src/main/java/com/polzzak/domain/notification/entity/NotificationType.java index b8391ba..2dd24d9 100644 --- a/src/main/java/com/polzzak/domain/notification/entity/NotificationType.java +++ b/src/main/java/com/polzzak/domain/notification/entity/NotificationType.java @@ -38,6 +38,15 @@ public String getMessageWithParameter(final String parameter) { return String.format(this.message, "" + parameter + ""); } + public String getParameterWithoutBold(final String parameter) { + if (this == STAMP_REQUEST || this == REWARD_REQUEST || this == STAMP_BOARD_COMPLETE || this == REWARDED + || this == REWARD_REQUEST_AGAIN || this == REWARD_FAIL || this == CREATED_STAMP_BOARD + || this == ISSUED_COUPON || this == REWARDED_REQUEST) { + return String.format(this.message, "'" + parameter + "'"); + } + return String.format(this.message, parameter); + } + public String getLinkWithParameter(final String parameter) { if (link == null) { return null; diff --git a/src/main/java/com/polzzak/domain/notification/handler/NotificationEventHandler.java b/src/main/java/com/polzzak/domain/notification/handler/NotificationEventHandler.java index b389541..43d0d14 100644 --- a/src/main/java/com/polzzak/domain/notification/handler/NotificationEventHandler.java +++ b/src/main/java/com/polzzak/domain/notification/handler/NotificationEventHandler.java @@ -5,10 +5,14 @@ import org.springframework.stereotype.Component; import com.polzzak.domain.notification.dto.NotificationCreateEvent; +import com.polzzak.domain.notification.dto.NotificationDto; import com.polzzak.domain.notification.dto.NotificationSettingDto; import com.polzzak.domain.notification.entity.Notification; import com.polzzak.domain.notification.entity.NotificationType; import com.polzzak.domain.notification.service.NotificationService; +import com.polzzak.domain.user.entity.Member; +import com.polzzak.domain.user.service.UserService; +import com.polzzak.global.infra.firebase.FirebaseCloudMessageService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -19,6 +23,8 @@ public class NotificationEventHandler { private final NotificationService notificationService; + private final FirebaseCloudMessageService firebaseCloudMessageService; + private final UserService userService; @Async @EventListener @@ -91,7 +97,9 @@ public void addNotification(NotificationCreateEvent event) { } } - notificationService.addNotification(event.senderId(), event.receiverId(), event.type(), event.data()); + Notification notification = notificationService.addNotification(event.senderId(), event.receiverId(), + event.type(), event.data()); + sendPushNotification(event.senderId(), notification); log.info("[NotificationEvent] info. sender_id : {}, receiver_id : {}, type : {}, data : {}", event.senderId(), event.receiverId(), event.type(), event.data()); @@ -99,4 +107,12 @@ public void addNotification(NotificationCreateEvent event) { log.error("[NotificationEvent] error.", e); } } + + private void sendPushNotification(long memberId, Notification notification) { + Member member = userService.findMemberByMemberId(memberId); + NotificationDto notificationDto = notificationService.getNotificationDto(member, notification, false); + + firebaseCloudMessageService.sendPushNotification(member, notificationDto.title(), notificationDto.message(), + notificationDto.link()); + } } diff --git a/src/main/java/com/polzzak/domain/notification/service/NotificationService.java b/src/main/java/com/polzzak/domain/notification/service/NotificationService.java index 349e814..b5f0e21 100644 --- a/src/main/java/com/polzzak/domain/notification/service/NotificationService.java +++ b/src/main/java/com/polzzak/domain/notification/service/NotificationService.java @@ -41,7 +41,7 @@ public class NotificationService { private final NotificationSettingRepository notificationSettingRepository; @Transactional - public void addNotification(final Long senderId, final Long receiverId, final NotificationType type, + public Notification addNotification(final Long senderId, final Long receiverId, final NotificationType type, final String data) { Member sender = userService.findMemberByMemberId(senderId); Member receiver = userService.findMemberByMemberId(receiverId); @@ -53,6 +53,7 @@ public void addNotification(final Long senderId, final Long receiverId, final No .data(data) .build(); notificationRepository.save(notification); + return notification; } @Transactional @@ -126,18 +127,26 @@ private NotificationResponse getNotificationResponse(final Long memberId, final pageRequest); List notificationDtoList = notifications.getContent().stream() - .map(notification -> getNotificationDto(member, notification)) + .map(notification -> getNotificationDto(member, notification, true)) .toList(); return NotificationResponse.from(pageRequest, notificationDtoList, notifications.hasNext()); } - private NotificationDto getNotificationDto(final Member member, final Notification notification) { + public NotificationDto getNotificationDto(final Member member, final Notification notification, + final boolean isBold) { Member sender = notification.getSender(); MemberDtoForNotification senderDto = sender == null ? null : MemberDtoForNotification.from(sender, fileClient.getSignedUrl(sender.getProfileKey())); - String message = notification.getType() - .getMessageWithParameter(getMessageParameter(member.getId(), notification)); + String message; + if (isBold) { + message = notification.getType() + .getMessageWithParameter(getMessageParameter(member.getId(), notification)); + } else { + message = notification.getType() + .getParameterWithoutBold(getMessageParameter(member.getId(), notification)); + } + String link = notification.getType().getLinkWithParameter(getLinkParameter(member.getId(), notification)); return NotificationDto.from(notification, message, link, senderDto); diff --git a/src/main/java/com/polzzak/domain/pushtoken/repository/PushTokenRepository.java b/src/main/java/com/polzzak/domain/pushtoken/repository/PushTokenRepository.java index 52e249e..d65e43e 100644 --- a/src/main/java/com/polzzak/domain/pushtoken/repository/PushTokenRepository.java +++ b/src/main/java/com/polzzak/domain/pushtoken/repository/PushTokenRepository.java @@ -1,9 +1,13 @@ package com.polzzak.domain.pushtoken.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import com.polzzak.domain.pushtoken.model.PushToken; +import com.polzzak.domain.user.entity.Member; public interface PushTokenRepository extends JpaRepository { + List getPushTokensByMember(Member member); } diff --git a/src/main/java/com/polzzak/domain/pushtoken/service/PushTokenService.java b/src/main/java/com/polzzak/domain/pushtoken/service/PushTokenService.java index e8f7718..daab872 100644 --- a/src/main/java/com/polzzak/domain/pushtoken/service/PushTokenService.java +++ b/src/main/java/com/polzzak/domain/pushtoken/service/PushTokenService.java @@ -1,5 +1,7 @@ package com.polzzak.domain.pushtoken.service; +import java.util.List; + import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; @@ -17,7 +19,7 @@ public class PushTokenService { private final UserService userService; private final PushTokenRepository pushTokenRepository; - public void addToken(Long memberId, String token) { + public void addToken(final Long memberId, final String token) { Member member = userService.findMemberByMemberId(memberId); PushToken pushToken = PushToken.createPushToken() @@ -31,4 +33,8 @@ public void addToken(Long memberId, String token) { } } + + public List getPushTokens(final Member member) { + return pushTokenRepository.getPushTokensByMember(member); + } } diff --git a/src/main/java/com/polzzak/global/infra/firebase/FirebaseCloudMessageService.java b/src/main/java/com/polzzak/global/infra/firebase/FirebaseCloudMessageService.java new file mode 100644 index 0000000..98c601c --- /dev/null +++ b/src/main/java/com/polzzak/global/infra/firebase/FirebaseCloudMessageService.java @@ -0,0 +1,68 @@ +package com.polzzak.global.infra.firebase; + +import java.io.FileInputStream; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.Notification; +import com.polzzak.domain.pushtoken.model.PushToken; +import com.polzzak.domain.pushtoken.service.PushTokenService; +import com.polzzak.domain.user.entity.Member; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class FirebaseCloudMessageService { + + private final PushTokenService pushTokenService; + + public void sendPushNotification(Member member, String title, String body, String link) { + try { + FileInputStream serviceAccount = new FileInputStream( + "src/main/resources/firebase/firebase_service_key.json"); + + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + + if (FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(options); + } + + List registrationTokens = pushTokenService.getPushTokens(member).stream() + .map(PushToken::getToken) + .toList(); + + MulticastMessage message = MulticastMessage.builder() + .setNotification(Notification.builder() + .setTitle(title) + .setBody(body) + .build()) + .addAllTokens(registrationTokens) + .putData("link", link) + .build(); + BatchResponse response = null; + try { + response = FirebaseMessaging.getInstance().sendEachForMulticast(message); + } catch (FirebaseMessagingException e) { + e.printStackTrace(); + } + // See the BatchResponse reference documentation + // for the contents of response. + System.out.println(response.getSuccessCount() + " messages were sent successfully"); + System.out.println(response); + + } catch (Exception e) { + + } + } +} diff --git a/src/main/resources/firebase/firebase_service_key.json b/src/main/resources/firebase/firebase_service_key.json new file mode 100644 index 0000000..f0043c0 --- /dev/null +++ b/src/main/resources/firebase/firebase_service_key.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "polzzak-57648", + "private_key_id": "ead4311d449ac5facc6c507ec152a6d9f54a279d", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuffOTpdr5Z23/\nn6c37jA+TkjDj8MZxBr67ovNVgKSUchidJe3A8VYS+Ck6tMaMr7/YPUu6OfkkHOS\n0VlW3UQwYRqj5Rk5kH6ByBxVH9ZI7Sh0FO9pH71dsHZXwVLam6UOKcyrrbJhwcfX\nXrnksdCi4LxhvIxTsn0z11Q3CXtf5diKXgIt+Ewgqk/sKWFmCLSjIATD6mMn5VTl\nD8+QyOCV/U6ec3CmXHj2RAJMm1FiIE0vHDaGwNtc6hha/igibK6OoQ5+lyrnvuMr\ngGS3o2zH5TOBfMs0FI9KrHaUgxfa0p9jG4Oze1ibyZhK+6hl1RSBSWuxy5iY77vp\n4fdrzzexAgMBAAECggEACaYdWgTJ3xDBHGmPraAWOtvJWkcQ2tPlSgr24BvpeH3d\nPtSDrzMeLovDmFsD4Wb8+NI7vKRUbmcufOfmsM77flFgT7/TbUN4O2T9bBeemdnD\naufddUq0BgJECQY/tqb0sZvOHZA1VQKKMnaigOr0Ro123VC30ckE82Ds3z4+/EZ5\n4+KKxVxXMiV3seQOHIdiMHI/ClrUTrr7S7h67UNJ6ZFzuP/JD1j7XENO9lM2haCL\ny3uxa7KEHDJWs1DU6O8FsXaxdb/XCMAYYTnH33SBtnUxgHNmdbv6IyZfTi/Wm+rY\nC8Vrua52T+8VRav1btVCVNQE6RpNWgWFGGq8pAy1oQKBgQDyDUNyfnfK/xZJAZ9N\nKMe7uBhrS95PAF33fHF6Kc70WPO9JLxWnXFNJPiT9bCRer7Vc9fkB388N43BnGX9\nYDR68EySd2GA8C2BloM6/pBULg7u4CxOrbcaWgqkY8oEktM7cgn5I2EVOGVwTfwP\n420ApXUS5zdUIGR/XsUROBf+hwKBgQC4jAyHNzGhczEALhbLTB+4gZqRO3J1z2uH\n4msAyyD5dU/Vrpa+i+HwG/wWELAzIRbFbwL8pEa4qtbD+ofDN3ytpn86KQZrwxK2\n0m89ajadwppeOh2z051TGCeU1cyECkv3iUPfZP+z9fERnNdAt+cly462zgaH+uJT\n2+/luP0uBwKBgQCJBhEkg4t1EyqecZioqWlIT1MjinNy7ZZEP+JNcdWCZci1TlKA\nBejZ7w/5UqB9+qqFU2rn34abpCdPbyYdZZTP87ClSYec4logfgAUKX+y58/0UltC\nvvxkooxbu1HlfOivQkN7EhgnVyG1jbAfnnNaZk/8P4AG07+QiymsMcEDiQKBgAyx\ntXrnlQZiAhDdGrxJNDVg1N0AldL8vYzPSkT3tAD0zNUJ+VyKCrSVeDWcWEJsGEDk\nbfQq6KJzPeqlJQmMm4rmVQIPKF3pQTRKLVSwJamcZTnuDXT9LWk11CMswbCjdK5G\nRuDq9ZvPYxGvFC9jdwbmhZ6VdWWNIFxcWJgYrXGpAoGBAOwyu9ycczHwhaiITVnH\nDmG7S2LCj8Jq3u3351c6dv/Lt8wuZ6u2FmTuMh8id9hD7u2TO9QzIF8+S10Rc/J8\nzb9GI3KLg67bCCrReZQ0B1L2cjKUObpx5om2icxYXTV1k3GyVg7RJwQ5NA8ncQSP\nvXysN6nV8rcLOSuK8iGsS9Je\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-mzmu4@polzzak-57648.iam.gserviceaccount.com", + "client_id": "101961288027838881996", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-mzmu4%40polzzak-57648.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +}