Skip to content

Commit

Permalink
Merge pull request #115 from Team-B1ND/Feature/#113
Browse files Browse the repository at this point in the history
Feature #113 : fcm 적용
  • Loading branch information
dongchandev authored Sep 23, 2024
2 parents 64f871b + 7aff261 commit 0f1e22b
Show file tree
Hide file tree
Showing 26 changed files with 324 additions and 16 deletions.
1 change: 1 addition & 0 deletions dodam-application/dodam-rest-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
implementation project(':dodam-in-system-available:dodam-neis-meal-client')
implementation project(':dodam-in-system-available:dodam-token-client')
implementation project(':dodam-in-system-available:dodam-youtube-video-client')
implementation project(':dodam-in-system-available:dodam-firebase-client')

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import b1nd.dodam.domain.rds.member.entity.Member;
import b1nd.dodam.domain.rds.member.repository.MemberRepository;
import b1nd.dodam.restapi.auth.application.data.req.LoginReq;
import b1nd.dodam.restapi.support.data.ResponseData;
import b1nd.dodam.restapi.support.encrypt.Sha512PasswordEncoder;
import b1nd.dodam.token.client.DodamTokenClient;
import b1nd.dodam.restapi.auth.application.data.req.ReissueTokenReq;
import b1nd.dodam.restapi.auth.application.data.res.LoginRes;
import b1nd.dodam.restapi.auth.application.data.res.ReissueTokenRes;
import b1nd.dodam.restapi.support.data.ResponseData;
import b1nd.dodam.restapi.support.encrypt.Sha512PasswordEncoder;
import b1nd.dodam.token.client.DodamTokenClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
Expand All @@ -35,6 +35,7 @@ public CompletableFuture<ResponseData<LoginRes>> login(LoginReq req) {
Member member = memberRepository.getById(req.id());
member.checkIfPasswordIsCorrect(Sha512PasswordEncoder.encode(req.pw()));
member.checkIfStatusIncorrect();
member.updatePushToken(req.pushToken());
return CompletableFuture.supplyAsync(() -> member, executor)
.thenCompose(m -> tokenClient.issueTokens(member.getId(), member.getRole().getNumber()))
.thenApply(tokens -> new LoginRes(member, tokens.accessToken(), tokens.refreshToken()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

import jakarta.validation.constraints.NotBlank;

public record LoginReq(@NotBlank String id, @NotBlank String pw) {}
public record LoginReq(@NotBlank String id, @NotBlank String pw, String pushToken) {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import b1nd.dodam.domain.rds.bus.entity.BusApplication;
import b1nd.dodam.domain.rds.bus.repository.BusApplicationRepository;
import b1nd.dodam.domain.rds.bus.repository.BusRepository;
import b1nd.dodam.domain.rds.member.entity.Member;
import b1nd.dodam.domain.rds.member.repository.StudentRepository;
import b1nd.dodam.firebase.client.FCMClient;
import b1nd.dodam.restapi.auth.infrastructure.security.support.MemberAuthenticationHolder;
import b1nd.dodam.restapi.bus.application.data.req.BusReq;
import b1nd.dodam.restapi.bus.application.data.res.BusMemberRes;
Expand All @@ -30,10 +32,13 @@ public class BusUseCase {
private final BusApplicationRepository busApplicationRepository;
private final StudentRepository studentRepository;
private final MemberAuthenticationHolder memberAuthenticationHolder;
private final FCMClient fcmClient;

@Transactional(rollbackFor = Exception.class)
public Response register(BusReq req) {
busRepository.save(req.mapToBus());
List<String> pushTokens = studentRepository.findAllMembers().stream().map(Member::getPushToken).toList();
fcmClient.sendMessages(pushTokens, "귀가버스 신청", "귀가 버스 신청이 가능해요! 신청해주세요.");
return Response.created("버스 등록 성공");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package b1nd.dodam.restapi.nightstudy.application;

import b1nd.dodam.domain.rds.nightstudy.entity.NightStudy;
import b1nd.dodam.domain.rds.nightstudy.service.NightStudyService;
import b1nd.dodam.firebase.client.FCMClient;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;

@Component
@RequiredArgsConstructor
public class NightStudyPushAlarmScheduler {
private final NightStudyService service;
private final FCMClient fcmClient;

@Scheduled(cron = "0 0 11 * * ?")
public void scheduledPushAlarm() {
List<NightStudy> nightStudies = service.getByEndDate(LocalDate.now().minusDays(1));
List<String> tokens = nightStudies.stream()
.map(n -> n.getStudent().getMember().getPushToken())
.toList();
fcmClient.sendMessages(tokens, "심야자습 만료", "심야자습이 만료됐어요.");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package b1nd.dodam.restapi.nightstudy.application;

import b1nd.dodam.core.util.ZonedDateTimeUtil;
import b1nd.dodam.domain.rds.member.entity.Member;
import b1nd.dodam.domain.rds.member.entity.Student;
import b1nd.dodam.domain.rds.member.entity.Teacher;
import b1nd.dodam.domain.rds.member.repository.StudentRepository;
Expand All @@ -16,6 +17,7 @@
import b1nd.dodam.restapi.nightstudy.application.data.res.NightStudyRes;
import b1nd.dodam.restapi.support.data.Response;
import b1nd.dodam.restapi.support.data.ResponseData;
import b1nd.dodam.restapi.support.pushalarm.PushAlarmEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -61,23 +63,27 @@ private void throwExceptionWhenStudentIsNotApplicant(NightStudy nightStudy, Stud
}
}

@PushAlarmEvent(target = "심야자습", status = ApprovalStatus.PENDING)
public Response allow(Long id) {
modifyStatus(id, ApprovalStatus.ALLOWED, null);
return Response.noContent("심야자습 승인 성공");
}

@PushAlarmEvent(target = "심야자습", status = ApprovalStatus.PENDING)
public Response reject(Long id, Optional<RejectNightStudyReq> req) {
modifyStatus(id, ApprovalStatus.REJECTED, req.map(RejectNightStudyReq::rejectReason).orElse(null));
return Response.noContent("심야자습 거절 성공");
}

@PushAlarmEvent(target = "심야자습", status = ApprovalStatus.PENDING)
public Response revert(Long id) {
modifyStatus(id, ApprovalStatus.PENDING, null);
return Response.noContent("심야자습 대기 성공");
}

private void modifyStatus(Long id, ApprovalStatus status, String rejectReason) {
Teacher teacher = teacherRepository.getByMember(memberAuthenticationHolder.current());
Member member = memberAuthenticationHolder.current();
Teacher teacher = teacherRepository.getByMember(member);
NightStudy nightStudy = nightStudyService.getBy(id);
nightStudy.modifyStatus(teacher, status, rejectReason);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package b1nd.dodam.restapi.outgoing.application;

import b1nd.dodam.core.util.ZonedDateTimeUtil;
import b1nd.dodam.domain.rds.member.entity.Member;
import b1nd.dodam.domain.rds.member.entity.Student;
import b1nd.dodam.domain.rds.member.enumeration.ActiveStatus;
import b1nd.dodam.domain.rds.member.repository.StudentRepository;
Expand All @@ -16,7 +17,10 @@
import b1nd.dodam.restapi.outgoing.application.data.res.OutGoingRes;
import b1nd.dodam.restapi.support.data.Response;
import b1nd.dodam.restapi.support.data.ResponseData;
import b1nd.dodam.restapi.support.pushalarm.ApprovalAlarmUtil;
import b1nd.dodam.restapi.support.pushalarm.PushAlarmEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -63,24 +67,28 @@ private void throwExceptionWhenStudentIsNotApplicant(OutGoing outGoing) {
}
}

@PushAlarmEvent(target = "외출", status = ApprovalStatus.ALLOWED)
public Response allow(Long id) {
modifyStatus(id, ApprovalStatus.ALLOWED, null);
return Response.noContent("외출 승인 성공");
}

@PushAlarmEvent(target = "외출", status = ApprovalStatus.REJECTED)
public Response reject(Long id, Optional<RejectOutGoingReq> req) {
modifyStatus(id, ApprovalStatus.REJECTED, req.map(RejectOutGoingReq::rejectReason).orElse(null));
return Response.noContent("외출 거절 성공");
}

@PushAlarmEvent(target = "외출", status = ApprovalStatus.PENDING)
public Response revert(Long id) {
modifyStatus(id, ApprovalStatus.PENDING, null);
return Response.noContent("외출 대기 성공");
}

private void modifyStatus(Long id, ApprovalStatus status, String rejectReason) {
Member member = memberAuthenticationHolder.current();
OutGoing outGoing = outGoingService.getById(id);
outGoing.modifyStatus(teacherRepository.getByMember(memberAuthenticationHolder.current()), status, rejectReason);
outGoing.modifyStatus(teacherRepository.getByMember(member), status, rejectReason);
}

@Transactional(readOnly = true)
Expand All @@ -96,5 +104,4 @@ public ResponseData<List<OutGoingRes>> getMy() {
LocalDateTime now = ZonedDateTimeUtil.nowToLocalDateTime();
return ResponseData.ok("내 외출 조회 성공", OutGoingRes.of(outGoingService.getByStudent(student, now)));
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package b1nd.dodam.restapi.outsleeping.application;

import b1nd.dodam.core.util.ZonedDateTimeUtil;
import b1nd.dodam.domain.rds.member.entity.Member;
import b1nd.dodam.domain.rds.member.entity.Student;
import b1nd.dodam.domain.rds.member.repository.StudentRepository;
import b1nd.dodam.domain.rds.member.repository.TeacherRepository;
Expand All @@ -14,7 +15,11 @@
import b1nd.dodam.restapi.outsleeping.application.data.res.OutSleepingRes;
import b1nd.dodam.restapi.support.data.Response;
import b1nd.dodam.restapi.support.data.ResponseData;
import b1nd.dodam.restapi.support.pushalarm.ApprovalAlarmEvent;
import b1nd.dodam.restapi.support.pushalarm.ApprovalAlarmUtil;
import b1nd.dodam.restapi.support.pushalarm.PushAlarmEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -52,24 +57,28 @@ public void throwExceptionWhenStudentIsNotApplicant(OutSleeping o) {
}
}

@PushAlarmEvent(target = "외박", status = ApprovalStatus.PENDING)
public Response allow(Long id) {
modifyStatus(id, ApprovalStatus.ALLOWED, null);
return Response.noContent("외박 승인 성공");
}

@PushAlarmEvent(target = "외박", status = ApprovalStatus.PENDING)
public Response reject(Long id, Optional<RejectOutSleepingReq> req) {
modifyStatus(id, ApprovalStatus.REJECTED, req.map(RejectOutSleepingReq::rejectReason).orElse(null));
return Response.noContent("외박 거절 성공");
}

@PushAlarmEvent(target = "외박", status = ApprovalStatus.PENDING)
public Response revert(Long id) {
modifyStatus(id, ApprovalStatus.PENDING, null);
return Response.noContent("외박 대기 성공");
}

private void modifyStatus(Long id, ApprovalStatus status, String rejectReason) {
Member member = memberAuthenticationHolder.current();
OutSleeping outSleeping = outSleepingService.getById(id);
outSleeping.modifyStatus(teacherRepository.getByMember(memberAuthenticationHolder.current()), status, rejectReason);
outSleeping.modifyStatus(teacherRepository.getByMember(member), status, rejectReason);
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package b1nd.dodam.restapi.support.pushalarm;

import b1nd.dodam.firebase.client.PushAlarmEvent;

public record ApprovalAlarmEvent(String pushToken, String title, String body) implements PushAlarmEvent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package b1nd.dodam.restapi.support.pushalarm;

import b1nd.dodam.domain.rds.support.enumeration.ApprovalStatus;

public final class ApprovalAlarmUtil {
public ApprovalAlarmUtil() {}

public static ApprovalAlarmEvent createAlarmEvent(String pushToken, String target, String rejectReason, ApprovalStatus status) {
return new ApprovalAlarmEvent(
pushToken,
target,
createBody(target, rejectReason, status)
);
}

private static String createBody(String target, String rejectReason, ApprovalStatus status){
return switch (status) {

case ALLOWED -> target+"이 승인됐어요!";
case REJECTED -> target+"이 거절됐어요\n"+ (rejectReason != null ? "사유: " + rejectReason : "");
case PENDING -> target+"이 승인 취소됐어요";
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package b1nd.dodam.restapi.support.pushalarm;

import b1nd.dodam.core.exception.global.InternalServerException;
import b1nd.dodam.domain.rds.member.entity.Student;
import b1nd.dodam.domain.rds.nightstudy.service.NightStudyService;
import b1nd.dodam.domain.rds.outgoing.service.OutGoingService;
import b1nd.dodam.domain.rds.outsleeping.service.OutSleepingService;
import b1nd.dodam.domain.rds.support.enumeration.ApprovalStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class PushAlarmAspect {
private final ApplicationEventPublisher eventPublisher;
private final NightStudyService nightStudyService;
private final OutGoingService outGoingService;
private final OutSleepingService outSleepingService;

@Around("@annotation(pushAlarmEvent)")
public Object handlePushAlarmEvent(ProceedingJoinPoint joinPoint, PushAlarmEvent pushAlarmEvent) {

Object result = proceedJoinPointSafely(joinPoint);

Long id = getIdFromArgs(joinPoint.getArgs());
String rejectReason = getRejectReasonFromArgs(joinPoint.getArgs());
String target = pushAlarmEvent.target();
ApprovalStatus status = pushAlarmEvent.status();

Student student = getStudentByTargetAndId(target, id);

sendPushAlarm(student, target, rejectReason, status);

return result;
}

private Object proceedJoinPointSafely(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new InternalServerException();
}
}

private Long getIdFromArgs(Object[] args) {
if (args[0] instanceof Long id) {
return id;
} else {
throw new InternalServerException();
}
}

private String getRejectReasonFromArgs(Object[] args) {
if (args.length > 1 && args[1] instanceof Optional<?> optionalArg) {
return (String) optionalArg.orElse(null);
}
return null;
}

private Student getStudentByTargetAndId(String target, Long id) {
return switch (target) {
case "외출" -> outGoingService.getById(id).getStudent();
case "외박" -> outSleepingService.getById(id).getStudent();
case "심야자습" -> nightStudyService.getBy(id).getStudent();
default -> throw new InternalServerException();
};
}

private void sendPushAlarm(Student student, String target, String rejectReason, ApprovalStatus status) {
String pushToken = student.getMember().getPushToken();
ApprovalAlarmEvent alarmEvent = ApprovalAlarmUtil.createAlarmEvent(pushToken, target, rejectReason, status);
eventPublisher.publishEvent(alarmEvent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package b1nd.dodam.restapi.support.pushalarm;

import b1nd.dodam.domain.rds.support.enumeration.ApprovalStatus;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PushAlarmEvent {
String target();
ApprovalStatus status();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package b1nd.dodam.restapi.support.pushalarm;

import b1nd.dodam.firebase.client.FCMClient;
import b1nd.dodam.firebase.client.PushAlarmEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
public class PushAlarmEventHandler {
private final FCMClient fcmClient;

@Async
@TransactionalEventListener
public void listen(PushAlarmEvent e){
fcmClient.sendMessage(e.pushToken(), e.title(), e.body());
}
}
Loading

0 comments on commit 0f1e22b

Please sign in to comment.