Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] fcm 푸시 알림 구현 #57

Merged
merged 11 commits into from
Jul 15, 2024
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
Loading