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 푸시 알림 스케줄러에 등록 #89

Merged
merged 16 commits into from
Nov 10, 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
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());
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved
}

@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 {
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved

@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
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
Loading