From 6ba0be4cd95429d1fa5990e114c3109475d2bc17 Mon Sep 17 00:00:00 2001 From: chaewonni <113420297+chaewonni@users.noreply.github.com> Date: Tue, 16 Jul 2024 04:06:39 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20fcm=20=ED=91=B8=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B5=AC=ED=98=84=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [chore] #56 add required dependency in build.gradle * [feat] #56 add FirebaseException and register in GlobalExceptionHandler * [feat] #56 create FirebaseConfig * [feat] #56 create FcmService * [feat] #56 create FcmMessageDto and FcmContent * [feat] #56 update yml * [feat] #56 add Async in fcm service * [feat] #56 add fcm exception * [feat] #56 create fcm notification logic * [fix] #56 move fcm related exception in business to firebase exception * [fix] #56 edit error code number --- SERVER_YML | 2 +- build.gradle | 5 +- .../server/advice/GlobalExceptionHandler.java | 8 +++ .../server/config/FirebaseConfig.java | 35 +++++++++++++ .../server/exception/FirebaseException.java | 11 ++++ .../exception/code/FirebaseErrorCode.java | 19 +++++++ .../server/external/FcmService.java | 51 +++++++++++++++++++ .../server/external/dto/FcmMessageDto.java | 19 +++++++ .../server/external/enums/FcmContent.java | 18 +++++++ .../repository/ParticipantRepository.java | 20 ++++++++ .../participant/ParticipantRetriever.java | 17 +++++++ .../participant/ParticipantService.java | 22 ++++++++ 12 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/kkumulkkum/server/config/FirebaseConfig.java create mode 100644 src/main/java/org/kkumulkkum/server/exception/FirebaseException.java create mode 100644 src/main/java/org/kkumulkkum/server/exception/code/FirebaseErrorCode.java create mode 100644 src/main/java/org/kkumulkkum/server/external/FcmService.java create mode 100644 src/main/java/org/kkumulkkum/server/external/dto/FcmMessageDto.java create mode 100644 src/main/java/org/kkumulkkum/server/external/enums/FcmContent.java diff --git a/SERVER_YML b/SERVER_YML index e66df20..153924a 160000 --- a/SERVER_YML +++ b/SERVER_YML @@ -1 +1 @@ -Subproject commit e66df2043a63b344bf91922326b21b91793722c6 +Subproject commit 153924ab06b6c533dfe966f9ecd2464305ac54c1 diff --git a/build.gradle b/build.gradle index b6dfc98..e1300a4 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,9 @@ dependencies { // AWS S3 implementation("software.amazon.awssdk:bom:2.21.0") implementation("software.amazon.awssdk:s3:2.21.0") + + // Firebase + implementation 'com.google.firebase:firebase-admin:9.2.0' } tasks.named('test') { @@ -74,7 +77,7 @@ tasks.named('test') { task copyYml(type: Copy) { copy { from './SERVER_YML' - include "*.yml" + include "**" into './src/main/resources' } } \ No newline at end of file diff --git a/src/main/java/org/kkumulkkum/server/advice/GlobalExceptionHandler.java b/src/main/java/org/kkumulkkum/server/advice/GlobalExceptionHandler.java index 394d87b..6de8966 100644 --- a/src/main/java/org/kkumulkkum/server/advice/GlobalExceptionHandler.java +++ b/src/main/java/org/kkumulkkum/server/advice/GlobalExceptionHandler.java @@ -66,6 +66,14 @@ public ResponseEntity handleAwsException(AwsException e) { .body(e.getErrorCode()); } + @ExceptionHandler(value = {FirebaseException.class}) + public ResponseEntity handleAwsException(FirebaseException e) { + log.error("GlobalExceptionHandler catch FirebaseException : {}", e.getErrorCode().getMessage()); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(e.getErrorCode()); + } + // 도메인 관련된 에러가 아닐 경우 @ExceptionHandler(value = {BusinessException.class}) public ResponseEntity handleBusinessException(BusinessException e) { diff --git a/src/main/java/org/kkumulkkum/server/config/FirebaseConfig.java b/src/main/java/org/kkumulkkum/server/config/FirebaseConfig.java new file mode 100644 index 0000000..366044b --- /dev/null +++ b/src/main/java/org/kkumulkkum/server/config/FirebaseConfig.java @@ -0,0 +1,35 @@ +package org.kkumulkkum.server.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.kkumulkkum.server.exception.FirebaseException; +import org.kkumulkkum.server.exception.code.FirebaseErrorCode; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class FirebaseConfig { + + @PostConstruct + public void initialize() { + try { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(new ClassPathResource("firebase/firebase_service_key.json").getInputStream())) + .build(); + if (FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(options); + log.info("FirebaseApp initialized {}", FirebaseApp.getInstance().getName()); + } + } catch (IOException e) { + log.error("FirebaseApp initialize failed : {}", e.getMessage()); + throw new FirebaseException(FirebaseErrorCode.NOT_FOUND_FIREBASE_JSON); + } + } +} + diff --git a/src/main/java/org/kkumulkkum/server/exception/FirebaseException.java b/src/main/java/org/kkumulkkum/server/exception/FirebaseException.java new file mode 100644 index 0000000..b8a4791 --- /dev/null +++ b/src/main/java/org/kkumulkkum/server/exception/FirebaseException.java @@ -0,0 +1,11 @@ +package org.kkumulkkum.server.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.kkumulkkum.server.exception.code.FirebaseErrorCode; + +@Getter +@RequiredArgsConstructor +public class FirebaseException extends RuntimeException { + private final FirebaseErrorCode errorCode; +} diff --git a/src/main/java/org/kkumulkkum/server/exception/code/FirebaseErrorCode.java b/src/main/java/org/kkumulkkum/server/exception/code/FirebaseErrorCode.java new file mode 100644 index 0000000..3a13bdf --- /dev/null +++ b/src/main/java/org/kkumulkkum/server/exception/code/FirebaseErrorCode.java @@ -0,0 +1,19 @@ +package org.kkumulkkum.server.exception.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum FirebaseErrorCode implements DefaultErrorCode { + // 400 BAD_REQUEST + FCM_ERROR(HttpStatus.BAD_REQUEST,40070,"fcm 토큰 오류입니다."), + // 404 NOT_FOUND + NOT_FOUND_FIREBASE_JSON(HttpStatus.NOT_FOUND, 40470, "FIREBASE JSON을 찾을 수 없습니다."), + ; + + private HttpStatus httpStatus; + private int code; + private String message; +} diff --git a/src/main/java/org/kkumulkkum/server/external/FcmService.java b/src/main/java/org/kkumulkkum/server/external/FcmService.java new file mode 100644 index 0000000..9edbdc0 --- /dev/null +++ b/src/main/java/org/kkumulkkum/server/external/FcmService.java @@ -0,0 +1,51 @@ +package org.kkumulkkum.server.external; + +import com.google.firebase.messaging.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.kkumulkkum.server.exception.BusinessException; +import org.kkumulkkum.server.exception.FirebaseException; +import org.kkumulkkum.server.exception.code.BusinessErrorCode; +import org.kkumulkkum.server.exception.code.FirebaseErrorCode; +import org.kkumulkkum.server.external.dto.FcmMessageDto; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FcmService { + + @Async + public void sendBulk( + final List fcmTokens, + final FcmMessageDto fcmMessageDto + ){ + MulticastMessage message = createBulkMessage(fcmTokens, fcmMessageDto); + try { + FirebaseMessaging.getInstance().sendMulticast(message); + } catch (FirebaseMessagingException e){ + throw new FirebaseException(FirebaseErrorCode.FCM_ERROR); + } + } + + private MulticastMessage createBulkMessage( + final List fcmTokens, + final FcmMessageDto fcmMessageDto + ){ + return MulticastMessage.builder() + .addAllTokens(fcmTokens) + .setNotification( + Notification.builder() + .setTitle(fcmMessageDto.title()) + .setBody(fcmMessageDto.body()) + .build() + ) + .putData("screen", fcmMessageDto.screen()) + .putData("promiseId", fcmMessageDto.promiseId().toString()) + .build(); + } +} + diff --git a/src/main/java/org/kkumulkkum/server/external/dto/FcmMessageDto.java b/src/main/java/org/kkumulkkum/server/external/dto/FcmMessageDto.java new file mode 100644 index 0000000..9e3fe21 --- /dev/null +++ b/src/main/java/org/kkumulkkum/server/external/dto/FcmMessageDto.java @@ -0,0 +1,19 @@ +package org.kkumulkkum.server.external.dto; + +import org.kkumulkkum.server.external.enums.FcmContent; + +public record FcmMessageDto( + String title, + String body, + String screen, + Long promiseId +) { + public static FcmMessageDto of(FcmContent fcmContent, Long promiseId) { + return new FcmMessageDto( + fcmContent.getTitle(), + fcmContent.getBody(), + fcmContent.getScreen(), + promiseId + ); + } +} diff --git a/src/main/java/org/kkumulkkum/server/external/enums/FcmContent.java b/src/main/java/org/kkumulkkum/server/external/enums/FcmContent.java new file mode 100644 index 0000000..08b2055 --- /dev/null +++ b/src/main/java/org/kkumulkkum/server/external/enums/FcmContent.java @@ -0,0 +1,18 @@ +package org.kkumulkkum.server.external.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum FcmContent { + + FIRST_PREPARATION("⏰ 누군가 준비를 시작했어요 ⏰", "첫 번째 사람이 준비를 시작했어요\uD83D\uDE35\\n어플로 들어가 누군지 확인해 보세요!", "readyStatus"), + FIRST_DEPARTURE("⏰ 누군가 이동을 시작했어요 ⏰", "첫 번째로 누군가 이동을 시작했어요\uD83D\uDE35\\n어플로 들어가 누군지 확인해 보세요!", "readyStatus"), + FIRST_ARRIVAL("⏰ 누군가 도착 했어요 ⏰", "누군가 약속 장소에 도착했어요!\uD83D\uDE35\\n도착한 사람이 누구인지 확인해보세요!", "readyStatus"), + ; + + private final String title; + private final String body; + private final String screen; +} diff --git a/src/main/java/org/kkumulkkum/server/repository/ParticipantRepository.java b/src/main/java/org/kkumulkkum/server/repository/ParticipantRepository.java index 4e6107f..32b7d21 100644 --- a/src/main/java/org/kkumulkkum/server/repository/ParticipantRepository.java +++ b/src/main/java/org/kkumulkkum/server/repository/ParticipantRepository.java @@ -44,4 +44,24 @@ public interface ParticipantRepository extends JpaRepository boolean existsByPromiseIdAndUserId(Long promiseId, Long userId); List findAllByPromiseId(Long promiseId); + + @Query("SELECT COUNT(p) FROM Participant p " + + "WHERE p.promise.id = :promiseId AND p.preparationStartAt IS NOT NULL") + int countFirstPreparationByPromiseId(Long promiseId); + + @Query("SELECT COUNT(p) FROM Participant p " + + "WHERE p.promise.id = :promiseId AND p.departureAt IS NOT NULL") + int countFirstDepartureByPromiseId(Long promiseId); + + @Query("SELECT COUNT(p) FROM Participant p " + + "WHERE p.promise.id = :promiseId AND p.arrivalAt IS NOT NULL") + int countFirstArrivalByPromiseId(Long promiseId); + + @Query("SELECT ui.fcmToken " + + "FROM Participant p " + + "JOIN Member m ON p.member.id = m.id " + + "JOIN UserInfo ui ON m.user.id = ui.user.id " + + "WHERE p.promise.id = :promiseId AND m.user.id != :userId") + List findFcmTokenByPromiseId(Long promiseId, Long userId); + } diff --git a/src/main/java/org/kkumulkkum/server/service/participant/ParticipantRetriever.java b/src/main/java/org/kkumulkkum/server/service/participant/ParticipantRetriever.java index cd92cff..363b05f 100644 --- a/src/main/java/org/kkumulkkum/server/service/participant/ParticipantRetriever.java +++ b/src/main/java/org/kkumulkkum/server/service/participant/ParticipantRetriever.java @@ -43,4 +43,21 @@ public boolean existsByPromiseIdAndUserId( ) { return participantRepository.existsByPromiseIdAndUserId(promiseId, userId); } + + public int countFirstPreparationByPromiseId(final Long promiseId) { + return participantRepository.countFirstPreparationByPromiseId(promiseId); + } + + public int countFirstDepartureByPromiseId(final Long promiseId) { + return participantRepository.countFirstDepartureByPromiseId(promiseId); + } + + public int countFirstArrivalByPromiseId(final Long promiseId) { + return participantRepository.countFirstArrivalByPromiseId(promiseId); + } + + public List findFcmTokenByPromiseId(final Long promiseId, final Long userId) { + return participantRepository.findFcmTokenByPromiseId(promiseId, userId); + } + } diff --git a/src/main/java/org/kkumulkkum/server/service/participant/ParticipantService.java b/src/main/java/org/kkumulkkum/server/service/participant/ParticipantService.java index 71dd72f..a76aff6 100644 --- a/src/main/java/org/kkumulkkum/server/service/participant/ParticipantService.java +++ b/src/main/java/org/kkumulkkum/server/service/participant/ParticipantService.java @@ -9,6 +9,9 @@ import org.kkumulkkum.server.dto.participant.response.*; import org.kkumulkkum.server.exception.ParticipantException; import org.kkumulkkum.server.exception.code.ParticipantErrorCode; +import org.kkumulkkum.server.external.FcmService; +import org.kkumulkkum.server.external.dto.FcmMessageDto; +import org.kkumulkkum.server.external.enums.FcmContent; import org.kkumulkkum.server.service.promise.PromiseRetriever; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,6 +29,7 @@ public class ParticipantService { private final ParticipantRetriever participantRetriever; private final ParticipantEditor participantEditor; private final PromiseRetriever promiseRetriever; + private final FcmService fcmService; @Transactional public void preparePromise( @@ -37,6 +41,12 @@ public void preparePromise( throw new ParticipantException(ParticipantErrorCode.INVALID_STATE); } participantEditor.preparePromise(participant); + + int preparationCount = participantRetriever.countFirstPreparationByPromiseId(promiseId); + if (preparationCount == 1) { + List fcmTokens = participantRetriever.findFcmTokenByPromiseId(promiseId, userId); + fcmService.sendBulk(fcmTokens, FcmMessageDto.of(FcmContent.FIRST_PREPARATION, promiseId)); + } } @Transactional @@ -49,6 +59,12 @@ public void departurePromise( throw new ParticipantException(ParticipantErrorCode.INVALID_STATE); } participantEditor.departurePromise(participant); + + int departureCount = participantRetriever.countFirstDepartureByPromiseId(promiseId); + if (departureCount == 1) { + List fcmTokens = participantRetriever.findFcmTokenByPromiseId(promiseId, userId); + fcmService.sendBulk(fcmTokens, FcmMessageDto.of(FcmContent.FIRST_DEPARTURE, promiseId)); + } } @Transactional @@ -61,6 +77,12 @@ public void arrivalPromise( throw new ParticipantException(ParticipantErrorCode.INVALID_STATE); } participantEditor.arrivalPromise(participant); + + int arrivalCount = participantRetriever.countFirstArrivalByPromiseId(promiseId); + if (arrivalCount == 1) { + List fcmTokens = participantRetriever.findFcmTokenByPromiseId(promiseId, userId); + fcmService.sendBulk(fcmTokens, FcmMessageDto.of(FcmContent.FIRST_ARRIVAL, promiseId)); + } } @Transactional(readOnly = true)