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

[BE] feat: 스태프 인증 처리시 FCM 을 통해 유저 핸드폰에 입장 처리 완료 메시지를 보낸다(#444) #449

Merged
merged 33 commits into from
Sep 26, 2023
Merged
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0b1bdb2
feat: FCMConfig 생성
BGuga Sep 15, 2023
fb75c77
chore: FCMConfig 패키지 이동
BGuga Sep 15, 2023
1ef3906
feat: MemberFCM 생성
BGuga Sep 15, 2023
2b8200b
feat: MemberFCMService 생성
BGuga Sep 15, 2023
4c5c2a5
feat: NotificationFacade 생성
BGuga Sep 15, 2023
f2ffd3f
feat: 로그인 시 fcm 을 등록한다.
BGuga Sep 15, 2023
c20c05e
feat: 입장 처리 시 EntryProcessEvent 를 발행한다.
BGuga Sep 15, 2023
bff5dd5
feat: 유저 탈퇴시 유저의 FCM 을 모두 삭제한다.
BGuga Sep 15, 2023
a304bce
feat: 테스트 환경에서 FirebaseMessaging 을 MockBean 으로 등록한다
BGuga Sep 15, 2023
7105313
chore: 테스트 컨벤션 적용
BGuga Sep 15, 2023
ceb3126
chore: Notification -> FCMNotificationFacade 네이밍 변견
BGuga Sep 15, 2023
e55658a
feat: flyway 추가
BGuga Sep 15, 2023
72c452d
chore: 마지막 줄 개행 추가
BGuga Sep 16, 2023
06d02b3
feat: submodule 업데이트
BGuga Sep 17, 2023
da113b8
refactor: 메서드 네이밍, 메서드 순서, 파라미터 순서 변경
BGuga Sep 17, 2023
062cce6
refactor: fcm bean 을 테스트에서 제외
BGuga Sep 20, 2023
c594282
feat: EventListener phase 명시
BGuga Sep 20, 2023
e5586c6
chore: FCMEventListener 네이밍 변경
BGuga Sep 20, 2023
40f4374
feat: MemberFCM 의 Member 의존성 제거
BGuga Sep 20, 2023
06b8c9d
chore: EntryProcessEvent 패키지 분리
BGuga Sep 20, 2023
7ea3fb0
refactor: AuthService 가 MemberFCM 을 의존하지 않도록 변경
BGuga Sep 20, 2023
67a58e0
feat: MemberFCM 빈 생성자 추가
BGuga Sep 20, 2023
9eb300d
chore: flyway version 변경
BGuga Sep 20, 2023
0eb447a
Merge branch 'dev' into feat/#444
BGuga Sep 20, 2023
e112d0e
feat: FCMChannel 을 Enum 으로 관리
BGuga Sep 20, 2023
5aaf339
chore: 메서드 접근자 및 네이밍 변경, log 메세지 변경
BGuga Sep 26, 2023
29c5c55
feat: local prod, dev 환경에서만 FCM Bean 들이 생성되도록 변경
BGuga Sep 26, 2023
1c77fc9
refactor: eventListen 로직을 비동기적으로 처리한다
BGuga Sep 26, 2023
894bbe0
refactor: LoginService 와 MemberFCMService 를 분리한다
BGuga Sep 26, 2023
903fc5f
chore: 파라미터 네이밍 변경
BGuga Sep 26, 2023
b40a7a3
chore: logger -> log 네이밍 변경
BGuga Sep 26, 2023
cc98d6e
chore: log 메시지 변경
BGuga Sep 26, 2023
38476b3
chore: flyway 버전 변경
BGuga Sep 26, 2023
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
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
@@ -47,6 +47,9 @@ dependencies {
// Flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

// Firebase
implementation 'com.google.firebase:firebase-admin:8.1.0'
}

tasks.named('test') {
Original file line number Diff line number Diff line change
@@ -2,9 +2,9 @@

import com.festago.auth.domain.AuthPayload;
import com.festago.auth.domain.Role;
import com.festago.auth.domain.SocialType;
import com.festago.auth.domain.UserInfo;
import com.festago.auth.dto.LoginMemberDto;
import com.festago.auth.dto.LoginRequest;
import com.festago.auth.dto.LoginResponse;
import org.springframework.stereotype.Service;

@@ -22,8 +22,9 @@ public AuthFacadeService(AuthService authService, OAuth2Clients oAuth2Clients,
this.authProvider = authProvider;
}

public LoginResponse login(LoginRequest request) {
LoginMemberDto loginMember = authService.login(getUserInfo(request));
public LoginResponse login(SocialType socialType, String oAuthToken) {
UserInfo userInfo = getUserInfo(socialType, oAuthToken);
LoginMemberDto loginMember = authService.login(userInfo);
String accessToken = getAccessToken(loginMember.memberId());
return LoginResponse.of(accessToken, loginMember);
}
@@ -32,9 +33,9 @@ private String getAccessToken(Long memberId) {
return authProvider.provide(new AuthPayload(memberId, Role.MEMBER));
}

private UserInfo getUserInfo(LoginRequest request) {
OAuth2Client oAuth2Client = oAuth2Clients.getClient(request.socialType());
return oAuth2Client.getUserInfo(request.accessToken());
private UserInfo getUserInfo(SocialType socialType, String oAuthToken) {
OAuth2Client oAuth2Client = oAuth2Clients.getClient(socialType);
return oAuth2Client.getUserInfo(oAuthToken);
}

public void deleteMember(Long memberId) {
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
import com.festago.common.exception.NotFoundException;
import com.festago.member.domain.Member;
import com.festago.member.repository.MemberRepository;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -24,12 +25,18 @@ public AuthService(MemberRepository memberRepository) {
}

public LoginMemberDto login(UserInfo userInfo) {
return memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), userInfo.socialType())
.map(LoginMemberDto::isExists)
.orElseGet(() -> {
Member member = signUp(userInfo);
return LoginMemberDto.isNew(member);
});
LoginMemberDto loginMemberDto = handleLoginRequest(userInfo);
return loginMemberDto;
}

public LoginMemberDto handleLoginRequest(UserInfo userInfo) {
Optional<Member> originMember = memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), userInfo.socialType());
if (originMember.isPresent()) {
Member member = originMember.get();
return LoginMemberDto.isExists(member);
}
Member newMember = signUp(userInfo);
return LoginMemberDto.isNew(newMember);
}

private Member signUp(UserInfo userInfo) {
3 changes: 2 additions & 1 deletion backend/src/main/java/com/festago/auth/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@

public record LoginRequest(
@NotNull(message = "socialType 은 null 일 수 없습니다.") SocialType socialType,
@NotNull(message = "acessToken 은 null 일 수 없습니다.") String accessToken) {
@NotNull(message = "acessToken 은 null 일 수 없습니다.") String accessToken,
@NotNull(message = "fcmToken 은 null 일 수 없습니다.") String fcmToken) {

}
Original file line number Diff line number Diff line change
@@ -56,7 +56,8 @@ public enum ErrorCode {
INVALID_ENTRY_CODE_OFFSET("올바르지 않은 입장코드 오프셋입니다."),
INVALID_ROLE_NAME("해당하는 Role이 없습니다."),
FOR_TEST_ERROR("테스트용 에러입니다."),
;
FAIL_SEND_FCM_MESSAGE("FCM Message 전송에 실패했습니다."),
FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다.");
Comment on lines +59 to +60
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
FAIL_SEND_FCM_MESSAGE("FCM Message 전송에 실패했습니다."),
FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다.");
FAIL_SEND_FCM_MESSAGE("FCM Message 전송에 실패했습니다."),
FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다."),
;


private final String message;

Original file line number Diff line number Diff line change
@@ -8,10 +8,12 @@
import com.festago.entry.dto.EntryCodeResponse;
import com.festago.entry.dto.TicketValidationRequest;
import com.festago.entry.dto.TicketValidationResponse;
import com.festago.entry.dto.event.EntryProcessEvent;
import com.festago.ticketing.domain.MemberTicket;
import com.festago.ticketing.repository.MemberTicketRepository;
import java.time.Clock;
import java.time.LocalDateTime;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@@ -21,11 +23,14 @@ public class EntryService {

private final EntryCodeManager entryCodeManager;
private final MemberTicketRepository memberTicketRepository;
private final ApplicationEventPublisher publisher;
private final Clock clock;

public EntryService(EntryCodeManager entryCodeManager, MemberTicketRepository memberTicketRepository, Clock clock) {
public EntryService(EntryCodeManager entryCodeManager, MemberTicketRepository memberTicketRepository,
ApplicationEventPublisher publisher, Clock clock) {
this.entryCodeManager = entryCodeManager;
this.memberTicketRepository = memberTicketRepository;
this.publisher = publisher;
this.clock = clock;
}

@@ -50,6 +55,7 @@ public TicketValidationResponse validate(TicketValidationRequest request) {
EntryCodePayload entryCodePayload = entryCodeManager.extract(request.code());
MemberTicket memberTicket = findMemberTicket(entryCodePayload.getMemberTicketId());
memberTicket.changeState(entryCodePayload.getEntryState());
publisher.publishEvent(new EntryProcessEvent(memberTicket.getOwner().getId()));
Copy link
Member

Choose a reason for hiding this comment

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

어떤 memberTicket에 대한 fcm인지는 구분해주지 않아도 되나요?!
(fcm 메세지에 memberTicketId는 함께 보내지 않아도 되나요?)

Copy link
Member Author

Choose a reason for hiding this comment

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

지금은 이벤트가 딱 하나니까 그냥 이벤트가 날라왔다? -> 다음 화면으로 이동 이렇게 구현되어 있어요!!
애쉬가 말씀 주신 것처럼 memberTicketId 를 같이 보내서 android 에서도 검증을 할 수 있게 만들면 더 좋겠네요 ㅎㅎ
이건 안드로이드 분들과 한번 같이 이야기해보져

return TicketValidationResponse.from(memberTicket);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.festago.entry.dto.event;

public record EntryProcessEvent(
Long memberId) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.festago.fcm.application;

import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import com.festago.entry.dto.event.EntryProcessEvent;
import com.festago.fcm.domain.FCMChannel;
import com.festago.fcm.dto.MemberFCMResponse;
import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.AndroidNotification;
import com.google.firebase.messaging.BatchResponse;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.SendResponse;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@Profile({"dev", "prod"})
public class FCMNotificationEventListener {

private static final Logger logger = LoggerFactory.getLogger(FCMNotificationEventListener.class);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private static final Logger logger = LoggerFactory.getLogger(FCMNotificationEventListener.class);
private static final Logger log = LoggerFactory.getLogger(FCMNotificationEventListener.class);

아래 코드와 통일하면 좋을 것 같아요!


private final FirebaseMessaging firebaseMessaging;
private final MemberFCMService memberFCMService;

public FCMNotificationEventListener(FirebaseMessaging firebaseMessaging, MemberFCMService memberFCMService) {
this.firebaseMessaging = firebaseMessaging;
this.memberFCMService = memberFCMService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void sendFcmNotification(EntryProcessEvent event) {
List<Message> messages = createMessages(getMemberFCMToken(event.memberId()), FCMChannel.NOT_DEFINED.name());
try {
BatchResponse batchResponse = firebaseMessaging.sendAll(messages);
checkAllSuccess(batchResponse, event.memberId());
} catch (FirebaseMessagingException e) {
logger.error("fail send FCM message", e);
throw new InternalServerException(ErrorCode.FAIL_SEND_FCM_MESSAGE);
}
}

private List<String> getMemberFCMToken(Long memberId) {
return memberFCMService.findMemberFCM(memberId).memberFCMs().stream()
.map(MemberFCMResponse::fcmToken)
.toList();
}

private List<Message> createMessages(List<String> tokens, String channelId) {
return tokens.stream()
.map(token -> createMessage(token, channelId))
.toList();
}

private Message createMessage(String token, String channelId) {
return Message.builder()
.setAndroidConfig(createAndroidConfig(channelId))
.setToken(token)
.build();
}

private AndroidConfig createAndroidConfig(String channelId) {
return AndroidConfig.builder()
.setNotification(createAndroidNotification(channelId))
.build();
}

private AndroidNotification createAndroidNotification(String channelId) {
return AndroidNotification.builder()
.setChannelId(channelId)
.build();
}

private void checkAllSuccess(BatchResponse batchResponse, Long memberId) {
List<SendResponse> failSend = batchResponse.getResponses().stream()
.filter(sendResponse -> !sendResponse.isSuccessful())
.toList();

logger.warn("member {} 에 대한 다음 요청들이 실패했습니다. {}", memberId, failSend);
throw new InternalServerException(ErrorCode.FAIL_SEND_FCM_MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.festago.fcm.application;

import com.festago.auth.application.AuthExtractor;
import com.festago.auth.domain.AuthPayload;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import com.festago.fcm.domain.MemberFCM;
import com.festago.fcm.dto.MemberFCMsResponse;
import com.festago.fcm.repository.MemberFCMRepository;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class MemberFCMService {

private static final Logger log = LoggerFactory.getLogger(MemberFCMService.class);
private static final int LEAST_MEMBER_FCM = 1;

private final MemberFCMRepository memberFCMRepository;
private final AuthExtractor authExtractor;

public MemberFCMService(MemberFCMRepository memberFCMRepository, AuthExtractor authExtractor) {
this.memberFCMRepository = memberFCMRepository;
this.authExtractor = authExtractor;
}

@Transactional(readOnly = true)
public MemberFCMsResponse findMemberFCM(Long memberId) {
List<MemberFCM> memberFCM = memberFCMRepository.findByMemberId(memberId);
if (memberFCM.size() < LEAST_MEMBER_FCM) {
log.error("member {} 의 FCM 코드가 발급되지 않았습니다.", memberId);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
log.error("member {} 의 FCM 코드가 발급되지 않았습니다.", memberId);
log.error("member {} 의 FCM 토큰이 발급되지 않았습니다.", memberId);

사소한건데 이거 어떤가요 ㅋㅋㅋ

throw new InternalServerException(ErrorCode.FCM_NOT_FOUND);
}
return MemberFCMsResponse.from(memberFCM);
}

@Async
public void saveMemberFCM(String accessToken, String fcmToken) {
AuthPayload authPayload = authExtractor.extract(accessToken);
Long memberId = authPayload.getMemberId();
memberFCMRepository.save(new MemberFCM(memberId, fcmToken));
}
}
58 changes: 58 additions & 0 deletions backend/src/main/java/com/festago/fcm/config/FCMConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.festago.fcm.config;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;

@Configuration
@Profile({"prod", "dev"})
public class FCMConfig {

@Value("${fcm.key.path}")
private String fcmPrivateKeyPath;

@Value("${fcm.key.scope}")
private String fireBaseScope;

@Bean
public FirebaseMessaging firebaseMessaging() throws IOException {
Optional<FirebaseApp> defaultFirebaseApp = defaultFirebaseApp();
if (defaultFirebaseApp.isPresent()) {
return FirebaseMessaging.getInstance(defaultFirebaseApp.get());
}
return FirebaseMessaging.getInstance(
FirebaseApp.initializeApp(createFirebaseOption())
);
}

private Optional<FirebaseApp> defaultFirebaseApp() {
List<FirebaseApp> firebaseAppList = FirebaseApp.getApps();
if (firebaseAppList == null || firebaseAppList.isEmpty()) {
return Optional.empty();
}
return firebaseAppList.stream()
.filter(firebaseApp -> firebaseApp.getName().equals(FirebaseApp.DEFAULT_APP_NAME))
.findAny();
}

private FirebaseOptions createFirebaseOption() throws IOException {
return FirebaseOptions.builder()
.setCredentials(createGoogleCredentials())
.build();
}

private GoogleCredentials createGoogleCredentials() throws IOException {
return GoogleCredentials
.fromStream(new ClassPathResource(fcmPrivateKeyPath).getInputStream())
.createScoped(fireBaseScope);
}
}
5 changes: 5 additions & 0 deletions backend/src/main/java/com/festago/fcm/domain/FCMChannel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.festago.fcm.domain;

public enum FCMChannel {
NOT_DEFINED;
}
Loading
Loading