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]#238 feature: 전체 푸시알림메시지 발송 #239

Merged
merged 2 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 35 additions & 0 deletions src/main/java/org/winey/server/controller/BroadCastController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.winey.server.controller;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.winey.server.common.dto.ApiResponse;
import org.winey.server.controller.request.broadcast.BroadCastAllUserDto;
import org.winey.server.exception.Success;
import org.winey.server.service.BroadCastService;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.firebase.messaging.FirebaseMessagingException;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/broadcast")
@Tag(name = "BroadCast", description = "위니 전체 푸시 API Document")
public class BroadCastController {
private final BroadCastService broadCastService;

@PostMapping("/send-all")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "전체 유저에게 메시지 발송 API", description = "전체 유저에게 메시지를 발송합니다.")
public ApiResponse sendMessageToEntireUser(@RequestBody BroadCastAllUserDto broadCastAllUserDto){
return ApiResponse.success(Success.SEND_ENTIRE_MESSAGE_SUCCESS, broadCastService.broadAllUser(broadCastAllUserDto));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.winey.server.controller.request.broadcast;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class BroadCastAllUserDto {
String title;

String message;
}
2 changes: 2 additions & 0 deletions src/main/java/org/winey/server/exception/Error.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public enum Error {
* 422 UNPROCESSABLE ENTITY
*/
UNPROCESSABLE_ENTITY_DELETE_EXCEPTION(HttpStatus.UNPROCESSABLE_ENTITY, "클라의 요청을 이해했지만 삭제하지 못했습니다."),
UNPROCESSABLE_FIND_USERS(HttpStatus.UNPROCESSABLE_ENTITY, "요청을 이해했지만 유저들을 찾을 수 없었습니다."),
UNPROCESSABLE_SEND_TO_FIREBASE(HttpStatus.UNPROCESSABLE_ENTITY, "파이어베이스로 전송하는 과정에서 에러가 발생했습니다."),

/**
* 500 INTERNAL SERVER ERROR
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/winey/server/exception/Success.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public enum Success {
BLOCK_USER_SUCCESS(HttpStatus.OK, "유저 차단 성공"),
GET_ACHIEVEMENT_STATUS_SUCCESS(HttpStatus.OK, "레벨 달성 현황 조회 성공"),

SEND_ENTIRE_MESSAGE_SUCCESS(HttpStatus.OK, "전체 메시지 전송 성공"),

/**
* 201 CREATED
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.lang.Nullable;
import org.winey.server.domain.feed.Feed;
import org.winey.server.domain.goal.Goal;
import org.winey.server.domain.recommend.Recommend;
Expand All @@ -18,6 +19,9 @@ public interface UserRepository extends Repository<User, Long> {
// READ
Optional<User> findByUserId(Long userId);

List<User> findByFcmTokenNotNull();


Boolean existsBySocialIdAndSocialType(String socialId, SocialType socialType);

Optional<User> findBySocialIdAndSocialType(String socialId, SocialType socialType);
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/org/winey/server/service/BroadCastService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.winey.server.service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.winey.server.common.dto.ApiResponse;
import org.winey.server.controller.request.broadcast.BroadCastAllUserDto;
import org.winey.server.domain.user.User;
import org.winey.server.exception.Error;
import org.winey.server.exception.Success;
import org.winey.server.exception.model.CustomException;
import org.winey.server.infrastructure.UserRepository;
import org.winey.server.service.message.SendAllFcmDto;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.sun.net.httpserver.Authenticator;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class BroadCastService {

private final FcmService fcmService;

private final UserRepository userRepository;

public ApiResponse broadAllUser(BroadCastAllUserDto broadCastAllUserDto){
List<User> allUser = userRepository.findByFcmTokenNotNull();
List<String> tokenList;
if (!allUser.isEmpty()){
try {
tokenList = allUser.stream().map(
User::getFcmToken).collect(Collectors.toList());
System.out.println(tokenList);
fcmService.sendAllByTokenList(
SendAllFcmDto.of(tokenList, broadCastAllUserDto.getTitle(), broadCastAllUserDto.getMessage()));
return ApiResponse.success(Success.SEND_ENTIRE_MESSAGE_SUCCESS,
Success.SEND_ENTIRE_MESSAGE_SUCCESS.getMessage());
}catch (FirebaseMessagingException | JsonProcessingException e){
return ApiResponse.error(Error.UNPROCESSABLE_SEND_TO_FIREBASE, Error.UNPROCESSABLE_SEND_TO_FIREBASE.getMessage());
}
}
return ApiResponse.error(Error.UNPROCESSABLE_FIND_USERS, Error.UNPROCESSABLE_FIND_USERS.getMessage());
}




}
70 changes: 63 additions & 7 deletions src/main/java/org/winey/server/service/FcmService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@
import okhttp3.Response;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.winey.server.service.message.FcmMessage;
import org.winey.server.service.message.FcmRequestDto;
import org.winey.server.service.message.SendAllFcmDto;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -104,13 +108,6 @@ public void sendByTokenList(List<String> tokenList) {
@Async
public CompletableFuture<Response> sendByToken(FcmRequestDto wineyNotification) throws JsonProcessingException {
// 메시지 만들기
// Message message = Message.builder()
// .putData("feedId", String.valueOf(wineyNotification.getFeedId()))
// .putData("notiType", String.valueOf(wineyNotification.getType()))
// .putData("title", "위니 제국의 편지가 도착했어요.")
// .putData("message" ,wineyNotification.getMessage())
// .setToken(wineyNotification.getToken()) <- 만약 여기 destination 부분만 수정해도 될 수도 있음. 가능성 생각하기
// .build();
String jsonMessage = makeSingleMessage(wineyNotification);
// 요청에 대한 응답을 받을 response
Response response;
Expand Down Expand Up @@ -182,5 +179,64 @@ private String makeSingleMessage(FcmRequestDto wineyNotification) throws JsonPro
}
}

// private List<FcmMessage> makeCustomMessages(SendAllFcmDto wineyNotification) throws JsonProcessingException {
// try {
// List<FcmMessage> messages = wineyNotification.getTokenList()
// .stream()
// .map(token -> FcmMessage.builder()
// .message(FcmMessage.Message.builder()
// .token(token) // 1:1 전송 시 반드시 필요한 대상 토큰 설정
// .data(FcmMessage.Data.builder()
// .title("위니 제국의 편지가 도착했어요.")
// .message(wineyNotification.getMessage())
// .feedId(null)
// .notiType(null)
// .build())
// .notification(FcmMessage.Notification.builder()
// .title("위니 제국의 편지가 도착했어요.")
// .body(wineyNotification.getMessage())
// .build())
// .build()
// ).validateOnly(false)
// .build()).collect(Collectors.toList());
// return messages;
// } catch (Exception e) {
// throw new IllegalArgumentException("JSON 처리 도중에 예외가 발생했습니다.");
// }
// }

public CompletableFuture<BatchResponse> sendAllByTokenList(SendAllFcmDto wineyNotification) throws JsonProcessingException, FirebaseMessagingException {
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = wineyNotification.getTokenList();
try {
MulticastMessage message = MulticastMessage.builder()
.putData("title", wineyNotification.getTitle())
.putData("message", wineyNotification.getMessage())
.setNotification(new Notification(wineyNotification.getTitle(), wineyNotification.getMessage()))
.addAllTokens(registrationTokens)
.build();
BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
if (response.getFailureCount() > 0) {
List<SendResponse> responses = response.getResponses();
List<String> failedTokens = new ArrayList<>();
for (int i = 0; i < responses.size(); i++) {
if (!responses.get(i).isSuccessful()) {
// The order of responses corresponds to the order of the registration tokens.
failedTokens.add(registrationTokens.get(i));
}
}

System.out.println("List of tokens that caused failures: " + failedTokens);
}
return CompletableFuture.completedFuture(response);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 CompletableFuture를 처음 봐서 검색해봤는데 반환된 Future를 기반으로 외부에서 작업을 완료하거나 추가하거나 그러는 것 같더라구요.. 여기서는 어떻게 사용하는 건가용..??!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 결론부터 말씀드리면 비동기식으로 구현해야할지 동기식으로 그냥 구현해도 될지에 대해서 고민을 하는 과정이 있었는데요. 다음과 같은 코드는 비동기식으로 구현을 한다고 가정을 했을 때의 오류를 컨트롤하고싶어서 만들어놨다가 뇌정지가 온 모습입니다.. ㅜ
저의 얕은 지식으로 알기로는 비동기식으로 구현을 하게되면 그냥 보통의 return값을 선언했을 때 이후의 예외처리를 할 수 없다고? 알고있었습니다.(다른 방식이 있을 수도 있을 것 같습니다..ㅜ) timeOut으로 종료되는 것이 아니면 외부에서 종료시킬 수도 없어서 저것을 사용하면 비동기작업인데도 불구하고 어떤 에러가 발생했을 때 그 에러를 서비스단으로 반환시켜서 그랬을 때의 에러를 서비스단에서 핸들링하던가 하고싶었는데 저희가 기획에서 요청을 받으면 그 때만 보내는 것이기에 비동기로 하는 것이 맞을지에 대해서도 의문이라서 저도 다른 의견들이 궁금합니다. (동기식으로 그냥 구현해도 상관없을 것 같다면 그냥 빼도 될 것 같습니다!) 저도 비동기와 관련된 내용들은 CS지식이 많이 부족하기 때문에 이참에 더 공부해보도록 할게요 ㅜ

} catch (Exception e){
log.info(e.getMessage());
}
return null;
}
private Notification convertToFirebaseNotification(FcmMessage.Notification customNotification) {
return new Notification(customNotification.getTitle(), customNotification.getBody());
}

}

23 changes: 23 additions & 0 deletions src/main/java/org/winey/server/service/message/SendAllFcmDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.winey.server.service.message;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.winey.server.domain.notification.NotiType;

import java.io.Serializable;
import java.util.List;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class SendAllFcmDto {
private List<String> tokenList;

private String title;

private String message;

public static SendAllFcmDto of(List<String> tokenList, String title, String message){
return new SendAllFcmDto(tokenList, title, message);
}
}
Loading