Skip to content

Commit

Permalink
[feat] fcm 푸시 알림 구현 (#57)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
chaewonni authored Jul 15, 2024
1 parent 59fc171 commit 6ba0be4
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 2 deletions.
2 changes: 1 addition & 1 deletion SERVER_YML
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -74,7 +77,7 @@ tasks.named('test') {
task copyYml(type: Copy) {
copy {
from './SERVER_YML'
include "*.yml"
include "**"
into './src/main/resources'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ public ResponseEntity<AwsErrorCode> handleAwsException(AwsException e) {
.body(e.getErrorCode());
}

@ExceptionHandler(value = {FirebaseException.class})
public ResponseEntity<FirebaseErrorCode> 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<BusinessErrorCode> handleBusinessException(BusinessException e) {
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/org/kkumulkkum/server/config/FirebaseConfig.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
51 changes: 51 additions & 0 deletions src/main/java/org/kkumulkkum/server/external/FcmService.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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();
}
}

Original file line number Diff line number Diff line change
@@ -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
);
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/kkumulkkum/server/external/enums/FcmContent.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,24 @@ public interface ParticipantRepository extends JpaRepository<Participant, Long>
boolean existsByPromiseIdAndUserId(Long promiseId, Long userId);

List<Participant> 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<String> findFcmTokenByPromiseId(Long promiseId, Long userId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> findFcmTokenByPromiseId(final Long promiseId, final Long userId) {
return participantRepository.findFcmTokenByPromiseId(promiseId, userId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -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<String> fcmTokens = participantRetriever.findFcmTokenByPromiseId(promiseId, userId);
fcmService.sendBulk(fcmTokens, FcmMessageDto.of(FcmContent.FIRST_PREPARATION, promiseId));
}
}

@Transactional
Expand All @@ -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<String> fcmTokens = participantRetriever.findFcmTokenByPromiseId(promiseId, userId);
fcmService.sendBulk(fcmTokens, FcmMessageDto.of(FcmContent.FIRST_DEPARTURE, promiseId));
}
}

@Transactional
Expand All @@ -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<String> fcmTokens = participantRetriever.findFcmTokenByPromiseId(promiseId, userId);
fcmService.sendBulk(fcmTokens, FcmMessageDto.of(FcmContent.FIRST_ARRIVAL, promiseId));
}
}

@Transactional(readOnly = true)
Expand Down

0 comments on commit 6ba0be4

Please sign in to comment.