Skip to content

Commit

Permalink
feat: fcm 푸시 알림 스케줄러에 등록 (#89)
Browse files Browse the repository at this point in the history
* feat: 푸시 알림 관련 enum 정의

* feat: 미션 인원 관련 enum 정의

* fix: 하루에 미션 인증 2번 방지

* feat: 푸시 알림 job 정의

* refactor: aop를 활용한 job 로깅

* refactor: 미션 인원 enum 활용하도록 수정

* feat: 미션 쿼리 추가 정의

* refactor: 함수 분리

* fix: 친구의 미션 참여 시 방장한테만 푸시

* refactor: 함수명 직관적으로 변경

* refactor: 비즈니스 로직 서비스 코드로 이동

* remove: 불필요한 구성 클래스 삭제

* refactor: 공통 로직 validator로 이동

* remove: 불필요한 코드 삭제

* refactor: 상수 사용하도록 수정

* comment: 메서드 설명 추가
  • Loading branch information
kimyu0218 authored Nov 10, 2024
1 parent 2077ef2 commit 4b91204
Show file tree
Hide file tree
Showing 16 changed files with 341 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.nexters.goalpanzi.application.mission;

import com.nexters.goalpanzi.application.firebase.TopicGenerator;
import com.nexters.goalpanzi.application.mission.dto.response.MemberRankResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionDetailResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse;
import com.nexters.goalpanzi.application.mission.event.JoinMissionEvent;
import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
Expand All @@ -12,13 +14,17 @@
import com.nexters.goalpanzi.exception.AlreadyExistsException;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_CANCELLATION_WARNING;
import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_READY;

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
Expand All @@ -31,6 +37,7 @@ public class MissionMemberService {
private final MemberRepository memberRepository;

private final ApplicationEventPublisher eventPublisher;
private final PushNotificationSender pushNotificationSender;

public MissionDetailResponse getJoinableMission(final InvitationCode invitationCode) {
missionValidator.validateJoinableMission(invitationCode);
Expand All @@ -45,8 +52,9 @@ public void joinMission(final Long memberId, final InvitationCode invitationCode
missionValidator.validateMaxPersonnel(mission);
missionMemberRepository.save(MissionMember.join(member, mission));

// TODO
// eventPublisher.publishEvent(new JoinMissionEvent(mission.getId(), "TODO deviceToken", member.getNickname()));
if (member.getDeviceToken() != null) {
eventPublisher.publishEvent(new JoinMissionEvent(mission.getId(), member.getDeviceToken(), member.getNickname()));
}
}

private Mission getMissionByCode(final InvitationCode invitationCode) {
Expand Down Expand Up @@ -118,4 +126,34 @@ public void viewMissionRank(final Long missionId, final Long memberId) {
MissionMember missionMember = missionMemberRepository.getMissionMember(memberId, missionId);
missionMember.checkCompleted();
}

@Transactional
public void sendReadyPushMessage() {
List<Mission> missions = missionRepository.getReadyMissions();
missions.forEach(mission -> {
if (mission.isReadyTime() && missionValidator.hasEnoughMember(mission.getId())) {
String topic = TopicGenerator.getTopic(mission.getId());
pushNotificationSender.sendGroupMessage(
MISSION_READY.getTitle(),
MISSION_READY.getBody(),
topic
);
}
});
}

@Transactional
public void sendCancellationWarningPushMessage() {
List<Mission> missions = missionRepository.getReadyMissions();
missions.forEach(mission -> {
if (mission.isReadyTime() && !missionValidator.hasEnoughMember(mission.getId())) {
String topic = TopicGenerator.getTopic(mission.getId());
pushNotificationSender.sendGroupMessage(
MISSION_CANCELLATION_WARNING.getTitle(),
MISSION_CANCELLATION_WARNING.getBody(),
topic
);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.stereotype.Component;

import static com.nexters.goalpanzi.domain.mission.Mission.MAX_MISSION_MEMBER;
import static com.nexters.goalpanzi.domain.mission.Mission.MIN_MISSION_MEMBER;

@RequiredArgsConstructor
@Component
Expand Down Expand Up @@ -38,6 +39,10 @@ public void validateMissionPeriod(final Mission mission) {
}
}

public boolean hasEnoughMember(final Long missionId) {
return getMissionMemberSize(missionId) >= MIN_MISSION_MEMBER;
}

private int getMissionMemberSize(final Long missionId) {
return missionMemberRepository.findAllByMissionId(missionId).size();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.nexters.goalpanzi.application.mission;

import com.nexters.goalpanzi.application.firebase.TopicGenerator;
import com.nexters.goalpanzi.application.mission.dto.request.CreateMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationQuery;
import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationQuery;
Expand All @@ -8,35 +9,41 @@
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationsResponse;
import com.nexters.goalpanzi.application.upload.ObjectStorageClient;
import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.firebase.PushNotificationMessage;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import com.nexters.goalpanzi.domain.mission.MissionMember;
import com.nexters.goalpanzi.domain.mission.MissionMembers;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import com.nexters.goalpanzi.domain.mission.MissionVerificationView;
import com.nexters.goalpanzi.domain.mission.*;
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationViewRepository;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.*;

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class MissionVerificationService {

private final MissionVerificationRepository missionVerificationRepository;
private final MissionRepository missionRepository;
private final MissionMemberRepository missionMemberRepository;
private final MissionVerificationViewRepository missionVerificationViewRepository;
private final MemberRepository memberRepository;

private final ObjectStorageClient objectStorageClient;
private final PushNotificationSender pushNotificationSender;

private final MissionVerificationValidator missionVerificationValidator;
private final MissionVerificationResponseSorter missionVerificationResponseSorter;
Expand Down Expand Up @@ -89,4 +96,66 @@ public void viewMissionVerification(final ViewMissionVerificationCommand command

missionVerificationViewRepository.save(new MissionVerificationView(missionVerification, member));
}

@Transactional
public void sendVerificationPushMessage() {
LocalDate today = LocalDate.now();
int hour = LocalDateTime.now().getHour();
List<Mission> missions = missionRepository.getInProgressMissions();

missions.forEach(mission -> {
if (mission.isMissionDay() && mission.isPushTime(hour)) {
List<MissionVerification> verifications = missionVerificationRepository.findAllByMissionIdAndDate(mission.getId(), today);
int verificationCount = verifications.size();
String topic = TopicGenerator.getTopic(mission.getId());

if (verificationCount == 0) {
sendNoOneVerifiedPushMessage(MISSION_NO_ONE_VERIFIED, topic);
} else {
sendVerifiedPushMessage(MISSION_VERIFIED, topic, verificationCount);
}
}
});
}

private void sendVerifiedPushMessage(final PushNotificationMessage message, final String topic, final int verificationCount) {
pushNotificationSender.sendGroupMessage(
message.getTitle(verificationCount),
message.getBody(),
topic
);
}

private void sendNoOneVerifiedPushMessage(final PushNotificationMessage message, final String topic) {
pushNotificationSender.sendGroupMessage(
message.getTitle(),
message.getBody(),
topic
);
}

@Transactional
public void sendVerificationWarningPushMessage() {
LocalDate today = LocalDate.now();
int hour = LocalDateTime.now().getHour();
List<Mission> missions = missionRepository.getInProgressMissions();

missions.forEach(mission -> {
if (mission.isMissionDay() && mission.isPushTime(hour)) {
List<MissionMember> missionMembers = missionMemberRepository.findAllByMissionId(mission.getId());

missionMembers.forEach(missionMember -> {
Member member = missionMember.getMember();
Optional<MissionVerification> verification = missionVerificationRepository.findByMemberIdAndMissionIdAndDate(member.getId(), mission.getId(), today);
if (verification.isEmpty() && member.getDeviceToken() != null) {
pushNotificationSender.sendIndividualMessage(
MISSION_VERIFICATION_WARNING.getTitle(),
MISSION_VERIFICATION_WARNING.getBody(),
member.getDeviceToken()
);
}
});
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import java.util.List;

import static com.nexters.goalpanzi.application.firebase.PushNotificationMessage.*;
import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.*;

@Slf4j
@Component
Expand All @@ -37,7 +35,7 @@ public class MissionMemberEventHandler {
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void handleCreateMissionEvent(final CreateMissionEvent event) {
missionMemberService.joinMission(event.memberId(), new InvitationCode(event.invitationCode()));
log.info("Handled JoinMissionEvent for memberId: {}", event.memberId());
log.info("Handled CreateMissionEvent for memberId: {}", event.memberId());
}

@Async
Expand All @@ -55,7 +53,7 @@ void handleDeleteMemberEvent(final DeleteMemberEvent event) {
void handleDeleteMissionEvent(final DeleteMissionEvent event) {
missionMemberService.deleteAllByMissionId(event.missionId());
missionVerificationService.deleteAllByMissionId(event.missionId());

pushNotificationSender.sendGroupMessage(
MISSION_DELETED.getTitle(),
MISSION_DELETED.getBody(),
Expand All @@ -68,13 +66,11 @@ void handleDeleteMissionEvent(final DeleteMissionEvent event) {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void handleJoinMissionEvent(final JoinMissionEvent event) {
String topic = TopicGenerator.getTopic(event.missionId());
pushNotificationSender.sendGroupMessage(
pushNotificationSender.sendIndividualMessage(
MISSION_JOINED.getTitle(),
MISSION_JOINED.getBody(event.nickname()),
topic
event.deviceToken()
);
topicSubscriber.subscribeToTopic(List.of(event.deviceToken()), topic);

log.info("Handled JoinMissionEvent for missionId: {}", event.missionId());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.nexters.goalpanzi.common.aop;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class JobLoggingAspect {

@Around("execution(* com.nexters.goalpanzi.schedule.*.executeInternal(..))")
public void execute(final ProceedingJoinPoint joinPoint) throws Throwable {
String jobName = joinPoint.getTarget().getClass().getSimpleName();

log.info("{} started.", jobName);

StopWatch stopWatch = new StopWatch();
stopWatch.start();

try {
joinPoint.proceed();
} catch (Exception e) {
log.error("Error occurred while executing {}", jobName, e);
}

stopWatch.stop();
log.info("{} finished. Elapsed time: {} ms", jobName, 0);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.nexters.goalpanzi.application.firebase;
package com.nexters.goalpanzi.domain.firebase;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
Expand All @@ -14,7 +14,7 @@ public enum PushNotificationMessage {

// 미션 진행 중
MISSION_VERIFICATION_WARNING("\u23F0 마감임박! 1시간 남았어요!\uD83E\uDDE8\uD83D\uDCA5", "지금 인증 안 하면 오늘은 인증 실패!ㅠㅠ"),
MISSION_VERIFIED("˗ˋˏ 와 ˎˊ˗ %s명이 벌써 인증 완료 ˗ˋˏ 와 ˎˊ˗ ", "지금 누가 앞서가는지 확인해볼까요?"),
MISSION_VERIFIED("˗ˋˏ 와 ˎˊ˗ %d명이 벌써 인증 완료 ˗ˋˏ 와 ˎˊ˗ ", "지금 누가 앞서가는지 확인해볼까요?"),
MISSION_NO_ONE_VERIFIED("잊었니?..\uD83C\uDF42", "아직 아무도 인증 안 했어요! 1빠로 인증해 모두를 앞서갈 타이밍!"),
MISSION_COMPLETED("아니 글쎄..걔가 결국 1등 했다고?! \uD83D\uDDEF\uFE0F", "첫 번째 미션 완수자 등장! 빠르게 확인해 보세요!"),
MISSION_DELETED("뭐? 미션 끝났다고? 너 누군데? \uD83D\uDC40", "방장이 미션을 끝냈어요! 다음 미션에서 새롭게 만나요!"),
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/nexters/goalpanzi/domain/firebase/PushTime.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.nexters.goalpanzi.domain.firebase;

import lombok.Getter;

@Getter
public enum PushTime {
MORNING(9),
AFTERNOON(15),
EVERYDAY(15);

private final int hour;

PushTime(final int hour) {
this.hour = hour;
}
}
Loading

0 comments on commit 4b91204

Please sign in to comment.