From 5098e07ef0411fa3218bf3a305f78b88717de2e0 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:25:06 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=ED=91=B8=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B4=80=EB=A0=A8=20enum=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../firebase/PushNotificationMessage.java | 4 ++-- .../goalpanzi/domain/firebase/PushTime.java | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) rename src/main/java/com/nexters/goalpanzi/{application => domain}/firebase/PushNotificationMessage.java (94%) create mode 100644 src/main/java/com/nexters/goalpanzi/domain/firebase/PushTime.java diff --git a/src/main/java/com/nexters/goalpanzi/application/firebase/PushNotificationMessage.java b/src/main/java/com/nexters/goalpanzi/domain/firebase/PushNotificationMessage.java similarity index 94% rename from src/main/java/com/nexters/goalpanzi/application/firebase/PushNotificationMessage.java rename to src/main/java/com/nexters/goalpanzi/domain/firebase/PushNotificationMessage.java index d69da85..234f117 100644 --- a/src/main/java/com/nexters/goalpanzi/application/firebase/PushNotificationMessage.java +++ b/src/main/java/com/nexters/goalpanzi/domain/firebase/PushNotificationMessage.java @@ -1,4 +1,4 @@ -package com.nexters.goalpanzi.application.firebase; +package com.nexters.goalpanzi.domain.firebase; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; @@ -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", "방장이 미션을 끝냈어요! 다음 미션에서 새롭게 만나요!"), diff --git a/src/main/java/com/nexters/goalpanzi/domain/firebase/PushTime.java b/src/main/java/com/nexters/goalpanzi/domain/firebase/PushTime.java new file mode 100644 index 0000000..20da630 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/domain/firebase/PushTime.java @@ -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; + } +} From 5ad475228b83275154fdce5ba2b4dee41027f41a Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:26:49 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=9D=B8?= =?UTF-8?q?=EC=9B=90=20=EA=B4=80=EB=A0=A8=20enum=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/MissionMemberCount.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java new file mode 100644 index 0000000..17d4b81 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java @@ -0,0 +1,14 @@ +package com.nexters.goalpanzi.domain.mission; + +import lombok.Getter; + +@Getter +public enum MissionMemberCount { + MIN(2); + + private final int count; + + MissionMemberCount(final int count) { + this.count = count; + } +} From 2a8bc242fa8d40015487dd74870ffc345159231d Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:28:12 +0900 Subject: [PATCH 03/16] =?UTF-8?q?fix:=20=ED=95=98=EB=A3=A8=EC=97=90=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EC=9D=B8=EC=A6=9D=202=EB=B2=88=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/repository/MissionMemberRepository.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java index 3d9d25a..97ced59 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionMemberRepository.java @@ -3,10 +3,11 @@ import com.nexters.goalpanzi.domain.mission.MissionMember; import com.nexters.goalpanzi.exception.ErrorCode; import com.nexters.goalpanzi.exception.NotFoundException; +import jakarta.persistence.LockModeType; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @@ -21,6 +22,7 @@ public interface MissionMemberRepository extends JpaRepository findAllWithMissionByMemberId(final Long memberId); + @Lock(LockModeType.PESSIMISTIC_WRITE) default MissionMember getMissionMember(final Long memberId, final Long missionId) { return findByMemberIdAndMissionId(memberId, missionId) .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_JOINED_MISSION_MEMBER)); From 608dfc6c2d9e19d8ddc6d9c2f8e386d6705509bd Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:29:26 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20=ED=91=B8=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20job=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MissionCancellationWarningPushJob.java | 56 +++++++++++++ .../schedule/MissionReadyPushJob.java | 48 ++++++++++++ .../schedule/MissionVerificationPushJob.java | 78 +++++++++++++++++++ .../MissionVerificationWarningPushJob.java | 72 +++++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java create mode 100644 src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java create mode 100644 src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java create mode 100644 src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java new file mode 100644 index 0000000..e57a7ac --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java @@ -0,0 +1,56 @@ +package com.nexters.goalpanzi.schedule; + +import com.nexters.goalpanzi.application.firebase.TopicGenerator; +import com.nexters.goalpanzi.domain.mission.Mission; +import com.nexters.goalpanzi.domain.mission.MissionMember; +import com.nexters.goalpanzi.domain.mission.MissionMemberCount; +import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; +import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; +import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_CANCELLATION_WARNING; + +@Slf4j +@RequiredArgsConstructor +@DisallowConcurrentExecution +@Component +public class MissionCancellationWarningPushJob extends AbstractJob implements CustomAutomationJob { + + private final MissionRepository missionRepository; + private final MissionMemberRepository missionMemberRepository; + + private final PushNotificationSender pushNotificationSender; + + @Override + protected ScheduleBuilder getScheduleBuilder() { + // 11:30, 23:30 마다 실행 + return CronScheduleBuilder.cronSchedule("0 30 11,23 * * ?") + .withMisfireHandlingInstructionDoNothing(); + } + + @Override + @Transactional + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + List missions = missionRepository.getReadyMissions(); + missions.forEach(mission -> { + List missionMembers = missionMemberRepository.findAllByMissionId(mission.getId()); + int memberCount = missionMembers.size(); + + if (mission.isReady() && memberCount < MissionMemberCount.MIN.getCount()) { + String topic = TopicGenerator.getTopic(mission.getId()); + pushNotificationSender.sendGroupMessage( + MISSION_CANCELLATION_WARNING.getTitle(), + MISSION_CANCELLATION_WARNING.getBody(), + topic + ); + } + }); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java new file mode 100644 index 0000000..17f27f6 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java @@ -0,0 +1,48 @@ +package com.nexters.goalpanzi.schedule; + +import com.nexters.goalpanzi.application.firebase.TopicGenerator; +import com.nexters.goalpanzi.domain.mission.Mission; +import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; +import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_READY; + +@Slf4j +@RequiredArgsConstructor +@DisallowConcurrentExecution +@Component +public class MissionReadyPushJob extends AbstractJob implements CustomAutomationJob { + + private final MissionRepository missionRepository; + + private final PushNotificationSender pushNotificationSender; + + @Override + protected ScheduleBuilder getScheduleBuilder() { + // 11:00, 23:00 마다 실행 + return CronScheduleBuilder.cronSchedule("0 0 11,23 * * ?") + .withMisfireHandlingInstructionDoNothing(); + } + + @Override + @Transactional + protected void executeInternal(final JobExecutionContext context) throws JobExecutionException { + List missions = missionRepository.getReadyMissions(); + missions.forEach(mission -> { + if (mission.isReady()) { + pushNotificationSender.sendGroupMessage( + MISSION_READY.getTitle(), + MISSION_READY.getBody(), + TopicGenerator.getTopic(mission.getId()) + ); + } + }); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java new file mode 100644 index 0000000..4811ec0 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java @@ -0,0 +1,78 @@ +package com.nexters.goalpanzi.schedule; + +import com.nexters.goalpanzi.application.firebase.TopicGenerator; +import com.nexters.goalpanzi.domain.firebase.PushNotificationMessage; +import com.nexters.goalpanzi.domain.mission.Mission; +import com.nexters.goalpanzi.domain.mission.MissionVerification; +import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; +import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationRepository; +import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_NO_ONE_VERIFIED; +import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_VERIFIED; + +@Slf4j +@RequiredArgsConstructor +@DisallowConcurrentExecution +@Component +public class MissionVerificationPushJob extends AbstractJob implements CustomAutomationJob { + + private final MissionRepository missionRepository; + private final MissionVerificationRepository missionVerificationRepository; + + private final PushNotificationSender pushNotificationSender; + + @Override + protected ScheduleBuilder getScheduleBuilder() { + // 09:00, 15:00 마다 실행 + return CronScheduleBuilder.cronSchedule("0 0 9,15 * * ?") + .withMisfireHandlingInstructionDoNothing(); + } + + @Override + @Transactional + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + LocalDate today = LocalDate.now(); + int hour = LocalDateTime.now().getHour(); + List missions = missionRepository.getInProgressMissions(); + + missions.forEach(mission -> { + if (mission.isMissionDay() && mission.isPushTime(hour)) { + List verifications = missionVerificationRepository.findAllByMissionIdAndDate(mission.getId(), today); + int verificationCount = verifications.size(); + String topic = TopicGenerator.getTopic(mission.getId()); + + if (verificationCount == 0) { + sendNoOneVerifiedMessage(MISSION_NO_ONE_VERIFIED, topic); + } else { + sendVerifiedMessage(MISSION_VERIFIED, topic, verificationCount); + } + } + }); + } + + private void sendVerifiedMessage(final PushNotificationMessage message, final String topic, final int verificationCount) { + pushNotificationSender.sendGroupMessage( + message.getTitle(verificationCount), + message.getBody(), + topic + ); + } + + private void sendNoOneVerifiedMessage(final PushNotificationMessage message, final String topic) { + pushNotificationSender.sendGroupMessage( + message.getTitle(), + message.getBody(), + topic + ); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java new file mode 100644 index 0000000..9570c35 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java @@ -0,0 +1,72 @@ +package com.nexters.goalpanzi.schedule; + +import com.nexters.goalpanzi.domain.member.Member; +import com.nexters.goalpanzi.domain.mission.Mission; +import com.nexters.goalpanzi.domain.mission.MissionMember; +import com.nexters.goalpanzi.domain.mission.MissionVerification; +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.infrastructure.firebase.PushNotificationSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.stereotype.Component; +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.MISSION_VERIFICATION_WARNING; + +@Slf4j +@RequiredArgsConstructor +@DisallowConcurrentExecution +@Component +public class MissionVerificationWarningPushJob extends AbstractJob implements CustomAutomationJob { + + private final MissionRepository missionRepository; + private final MissionMemberRepository missionMemberRepository; + private final MissionVerificationRepository missionVerificationRepository; + + private final PushNotificationSender pushNotificationSender; + + @Override + protected ScheduleBuilder getScheduleBuilder() { + // 11:00, 23:00 마다 실행 + return CronScheduleBuilder.cronSchedule("0 0 11,23 * * ?") + .withMisfireHandlingInstructionDoNothing(); + } + + @Override + @Transactional + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + LocalDate today = LocalDate.now(); + int hour = LocalDateTime.now().getHour(); + List missions = missionRepository.getInProgressMissions(); + + missions.forEach(mission -> { + if (mission.isMissionDay() && mission.isPushTime(hour)) { + List missionMembers = missionMemberRepository.findAllByMissionId(mission.getId()); + + missionMembers.forEach(missionMember -> { + Member member = missionMember.getMember(); + Optional verification = missionVerificationRepository.findByMemberIdAndMissionIdAndDate(member.getId(), mission.getId(), today); + if (verification.isEmpty() && member.getDeviceToken() != null) { + sendMessage(member); + } + }); + } + }); + } + + private void sendMessage(final Member member) { + pushNotificationSender.sendIndividualMessage( + MISSION_VERIFICATION_WARNING.getTitle(), + MISSION_VERIFICATION_WARNING.getBody(), + member.getDeviceToken() + ); + } +} From 4e9da91bc9ed42dc3a82a11240544985f580e0a3 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:30:18 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20aop=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20job=20=EB=A1=9C=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/aop/JobLoggingAspect.java | 33 +++++++++++++++++++ .../nexters/goalpanzi/config/AopConfig.java | 9 +++++ .../goalpanzi/schedule/MissionStatusJob.java | 21 ++---------- 3 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/nexters/goalpanzi/common/aop/JobLoggingAspect.java create mode 100644 src/main/java/com/nexters/goalpanzi/config/AopConfig.java diff --git a/src/main/java/com/nexters/goalpanzi/common/aop/JobLoggingAspect.java b/src/main/java/com/nexters/goalpanzi/common/aop/JobLoggingAspect.java new file mode 100644 index 0000000..2bacb72 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/common/aop/JobLoggingAspect.java @@ -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); + } +} diff --git a/src/main/java/com/nexters/goalpanzi/config/AopConfig.java b/src/main/java/com/nexters/goalpanzi/config/AopConfig.java new file mode 100644 index 0000000..8e8d5e7 --- /dev/null +++ b/src/main/java/com/nexters/goalpanzi/config/AopConfig.java @@ -0,0 +1,9 @@ +package com.nexters.goalpanzi.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@Configuration +@EnableAspectJAutoProxy +public class AopConfig { +} diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionStatusJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionStatusJob.java index 7e03b3e..1c7f76d 100644 --- a/src/main/java/com/nexters/goalpanzi/schedule/MissionStatusJob.java +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionStatusJob.java @@ -3,12 +3,7 @@ import com.nexters.goalpanzi.application.mission.MissionMemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.time.StopWatch; -import org.quartz.CronScheduleBuilder; -import org.quartz.CronTrigger; -import org.quartz.DisallowConcurrentExecution; -import org.quartz.JobExecutionContext; -import org.quartz.ScheduleBuilder; +import org.quartz.*; import org.springframework.stereotype.Component; @Slf4j @@ -28,18 +23,6 @@ protected ScheduleBuilder getScheduleBuilder() { @Override protected void executeInternal(final JobExecutionContext context) { - log.info("MissionStatusJob started."); - - StopWatch stopWatch = new StopWatch(); - stopWatch.start(); - - try { - missionMemberService.batchUpdateStatus(); - } catch (Exception e) { - log.error("Error occurred while executing MissionStatusJob", e); - } - - stopWatch.stop(); // 타이머 종료 - log.info("MissionStatusJob finished. Elapsed time: {} ms", stopWatch.getTime()); + missionMemberService.batchUpdateStatus(); } } From 4d7329e94707d52c1830abc744ee6340950f66c3 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:31:14 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=9D=B8=EC=9B=90=20enum=20=ED=99=9C=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nexters/goalpanzi/domain/mission/MissionStatus.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java index 199b552..ecdb102 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java @@ -44,11 +44,11 @@ public static MissionStatus fromMission( return CREATED; } - if (mission.isMissionPeriod() && currentMemberCount <= 1) { + if (mission.isMissionPeriod() && currentMemberCount < MissionMemberCount.MIN.getCount()) { return CANCELED; } - if (mission.isMissionPeriod() && currentMemberCount > 1) { + if (mission.isMissionPeriod() && currentMemberCount >= MissionMemberCount.MIN.getCount()) { return IN_PROGRESS; } From 591a7d7b05d0e193a936759aaf31bc9835ad09d2 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:31:56 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../goalpanzi/domain/mission/Mission.java | 21 ++++++++++++++++++- .../mission/repository/MissionRepository.java | 18 +++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java index 790ed11..b84d6f3 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java @@ -2,16 +2,18 @@ import com.nexters.goalpanzi.common.time.TimeUtil; import com.nexters.goalpanzi.domain.common.BaseEntity; +import com.nexters.goalpanzi.domain.firebase.PushTime; import com.nexters.goalpanzi.infrastructure.jpa.DaysOfWeekConverter; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLRestriction; -import org.joda.time.LocalTime; +import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.List; import java.util.Objects; @@ -147,6 +149,23 @@ public LocalDateTime getMissionUploadEndDateTime() { ); } + public boolean isReady() { + LocalDateTime startTime = LocalDateTime.of(this.missionStartDate.toLocalDate(), LocalTime.parse(this.uploadStartTime)); + Duration duration = Duration.between(startTime, LocalDate.now()); + + return duration.isNegative() && duration.toHours() <= 1; + } + + public boolean isPushTime(final int hour) { + if (this.uploadStartTime.equals(TimeOfDay.MORNING.getStartTime())) { + return hour == PushTime.MORNING.getHour(); + } + if (this.uploadStartTime.equals(TimeOfDay.AFTERNOON.getStartTime())) { + return hour == PushTime.AFTERNOON.getHour(); + } + return hour == PushTime.EVERYDAY.getHour(); + } + @Override public boolean equals(final Object o) { if (this == o) return true; diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java index c908750..72a7d18 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java @@ -2,19 +2,35 @@ import com.nexters.goalpanzi.domain.mission.InvitationCode; import com.nexters.goalpanzi.domain.mission.Mission; -import com.nexters.goalpanzi.exception.BaseException; import com.nexters.goalpanzi.exception.ErrorCode; import com.nexters.goalpanzi.exception.NotFoundException; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface MissionRepository extends JpaRepository { Optional findByInvitationCode(final InvitationCode invitationCode); + List findByMissionStartDateGreaterThanEqual(final LocalDateTime todayStart); + + List findByMissionStartDateGreaterThanEqualAndMissionEndDateLessThanEqual(final LocalDateTime startDate, final LocalDateTime endDate); + default Mission getMission(final Long missionId) { return findById(missionId) .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MISSION, missionId)); } + + default List getReadyMissions() { + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); + return findByMissionStartDateGreaterThanEqual(todayStart); + } + + default List getInProgressMissions() { + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); + return findByMissionStartDateGreaterThanEqualAndMissionEndDateLessThanEqual(todayStart, todayStart); + } } From b1a4c21cdb8c6ce1acac27dcfa024c090ff04aad Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:50:59 +0900 Subject: [PATCH 08/16] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MissionCancellationWarningPushJob.java | 12 ++++++++---- .../goalpanzi/schedule/MissionReadyPushJob.java | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java index e57a7ac..bcd228a 100644 --- a/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java @@ -40,10 +40,7 @@ protected ScheduleBuilder getScheduleBuilder() { protected void executeInternal(JobExecutionContext context) throws JobExecutionException { List missions = missionRepository.getReadyMissions(); missions.forEach(mission -> { - List missionMembers = missionMemberRepository.findAllByMissionId(mission.getId()); - int memberCount = missionMembers.size(); - - if (mission.isReady() && memberCount < MissionMemberCount.MIN.getCount()) { + if (mission.isReadyTime() && !hasEnoughMember(mission.getId())) { String topic = TopicGenerator.getTopic(mission.getId()); pushNotificationSender.sendGroupMessage( MISSION_CANCELLATION_WARNING.getTitle(), @@ -53,4 +50,11 @@ protected void executeInternal(JobExecutionContext context) throws JobExecutionE } }); } + + private boolean hasEnoughMember(final Long missionId) { + List missionMembers = missionMemberRepository.findAllByMissionId(missionId); + int memberCount = missionMembers.size(); + + return memberCount >= MissionMemberCount.MIN.getCount(); + } } diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java index 17f27f6..efd4252 100644 --- a/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java @@ -2,6 +2,9 @@ import com.nexters.goalpanzi.application.firebase.TopicGenerator; import com.nexters.goalpanzi.domain.mission.Mission; +import com.nexters.goalpanzi.domain.mission.MissionMember; +import com.nexters.goalpanzi.domain.mission.MissionMemberCount; +import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; import lombok.RequiredArgsConstructor; @@ -21,6 +24,7 @@ public class MissionReadyPushJob extends AbstractJob implements CustomAutomationJob { private final MissionRepository missionRepository; + private final MissionMemberRepository missionMemberRepository; private final PushNotificationSender pushNotificationSender; @@ -36,13 +40,21 @@ protected ScheduleBuilder getScheduleBuilder() { protected void executeInternal(final JobExecutionContext context) throws JobExecutionException { List missions = missionRepository.getReadyMissions(); missions.forEach(mission -> { - if (mission.isReady()) { + if (mission.isReadyTime() && hasEnoughMember(mission.getId())) { + String topic = TopicGenerator.getTopic(mission.getId()); pushNotificationSender.sendGroupMessage( MISSION_READY.getTitle(), MISSION_READY.getBody(), - TopicGenerator.getTopic(mission.getId()) + topic ); } }); } + + private boolean hasEnoughMember(final Long missionId) { + List missionMembers = missionMemberRepository.findAllByMissionId(missionId); + int memberCount = missionMembers.size(); + + return memberCount >= MissionMemberCount.MIN.getCount(); + } } From 25055d14591f0892c6ed1d5baad4af7a71f4232e Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:57:25 +0900 Subject: [PATCH 09/16] =?UTF-8?q?fix:=20=EC=B9=9C=EA=B5=AC=EC=9D=98=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EC=B0=B8=EC=97=AC=20=EC=8B=9C=20=EB=B0=A9?= =?UTF-8?q?=EC=9E=A5=ED=95=9C=ED=85=8C=EB=A7=8C=20=ED=91=B8=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/handler/MissionMemberEventHandler.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandler.java b/src/main/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandler.java index 0333d93..d933994 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandler.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandler.java @@ -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 @@ -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 @@ -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(), @@ -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()); } From a0ebd15643d65877685a13370910eed112090670 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Thu, 7 Nov 2024 18:59:31 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EC=A7=81=EA=B4=80=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java index b84d6f3..21a901e 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java @@ -149,7 +149,7 @@ public LocalDateTime getMissionUploadEndDateTime() { ); } - public boolean isReady() { + public boolean isReadyTime() { LocalDateTime startTime = LocalDateTime.of(this.missionStartDate.toLocalDate(), LocalTime.parse(this.uploadStartTime)); Duration duration = Duration.between(startTime, LocalDate.now()); From 20387f656bf3bdcf4d4564c358c388d01dce127d Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Sun, 10 Nov 2024 20:15:54 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor:=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/MissionMemberService.java | 50 +++++++++++- .../mission/MissionVerificationService.java | 77 ++++++++++++++++++- .../MissionCancellationWarningPushJob.java | 36 +-------- .../schedule/MissionReadyPushJob.java | 38 +-------- .../schedule/MissionVerificationPushJob.java | 56 +------------- .../MissionVerificationWarningPushJob.java | 50 +----------- 6 files changed, 133 insertions(+), 174 deletions(-) diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java index dfd93ab..f506913 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java @@ -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; @@ -12,6 +14,7 @@ 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; @@ -19,6 +22,9 @@ 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 @@ -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); @@ -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) { @@ -118,4 +126,42 @@ public void viewMissionRank(final Long missionId, final Long memberId) { MissionMember missionMember = missionMemberRepository.getMissionMember(memberId, missionId); missionMember.checkCompleted(); } + + @Transactional + public void sendReadyPushMessage() { + List missions = missionRepository.getReadyMissions(); + missions.forEach(mission -> { + if (mission.isReadyTime() && hasEnoughMember(mission.getId())) { + String topic = TopicGenerator.getTopic(mission.getId()); + pushNotificationSender.sendGroupMessage( + MISSION_READY.getTitle(), + MISSION_READY.getBody(), + topic + ); + } + }); + } + + @Transactional + public void sendCancellationWarningPushMessage() { + List missions = missionRepository.getReadyMissions(); + missions.forEach(mission -> { + if (mission.isReadyTime() && !hasEnoughMember(mission.getId())) { + String topic = TopicGenerator.getTopic(mission.getId()); + pushNotificationSender.sendGroupMessage( + MISSION_CANCELLATION_WARNING.getTitle(), + MISSION_CANCELLATION_WARNING.getBody(), + topic + ); + } + }); + } + + private boolean hasEnoughMember(final Long missionId) { + List missionMembers = missionMemberRepository.findAllByMissionId(missionId); + int memberCount = missionMembers.size(); + + return memberCount >= MissionMemberCount.MIN.getCount(); + } + } diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java index 580b3c3..7534d04 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionVerificationService.java @@ -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; @@ -8,23 +9,27 @@ 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 @@ -32,11 +37,13 @@ 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; @@ -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 missions = missionRepository.getInProgressMissions(); + + missions.forEach(mission -> { + if (mission.isMissionDay() && mission.isPushTime(hour)) { + List 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 missions = missionRepository.getInProgressMissions(); + + missions.forEach(mission -> { + if (mission.isMissionDay() && mission.isPushTime(hour)) { + List missionMembers = missionMemberRepository.findAllByMissionId(mission.getId()); + + missionMembers.forEach(missionMember -> { + Member member = missionMember.getMember(); + Optional 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() + ); + } + }); + } + }); + } } diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java index bcd228a..9a8b822 100644 --- a/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionCancellationWarningPushJob.java @@ -1,32 +1,19 @@ package com.nexters.goalpanzi.schedule; -import com.nexters.goalpanzi.application.firebase.TopicGenerator; -import com.nexters.goalpanzi.domain.mission.Mission; -import com.nexters.goalpanzi.domain.mission.MissionMember; -import com.nexters.goalpanzi.domain.mission.MissionMemberCount; -import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; -import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; -import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; +import com.nexters.goalpanzi.application.mission.MissionMemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.*; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - -import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_CANCELLATION_WARNING; - @Slf4j @RequiredArgsConstructor @DisallowConcurrentExecution @Component public class MissionCancellationWarningPushJob extends AbstractJob implements CustomAutomationJob { - private final MissionRepository missionRepository; - private final MissionMemberRepository missionMemberRepository; - - private final PushNotificationSender pushNotificationSender; + private final MissionMemberService missionMemberService; @Override protected ScheduleBuilder getScheduleBuilder() { @@ -38,23 +25,6 @@ protected ScheduleBuilder getScheduleBuilder() { @Override @Transactional protected void executeInternal(JobExecutionContext context) throws JobExecutionException { - List missions = missionRepository.getReadyMissions(); - missions.forEach(mission -> { - if (mission.isReadyTime() && !hasEnoughMember(mission.getId())) { - String topic = TopicGenerator.getTopic(mission.getId()); - pushNotificationSender.sendGroupMessage( - MISSION_CANCELLATION_WARNING.getTitle(), - MISSION_CANCELLATION_WARNING.getBody(), - topic - ); - } - }); - } - - private boolean hasEnoughMember(final Long missionId) { - List missionMembers = missionMemberRepository.findAllByMissionId(missionId); - int memberCount = missionMembers.size(); - - return memberCount >= MissionMemberCount.MIN.getCount(); + missionMemberService.sendCancellationWarningPushMessage(); } } diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java index efd4252..1f7949d 100644 --- a/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionReadyPushJob.java @@ -1,21 +1,10 @@ package com.nexters.goalpanzi.schedule; -import com.nexters.goalpanzi.application.firebase.TopicGenerator; -import com.nexters.goalpanzi.domain.mission.Mission; -import com.nexters.goalpanzi.domain.mission.MissionMember; -import com.nexters.goalpanzi.domain.mission.MissionMemberCount; -import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; -import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; -import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; +import com.nexters.goalpanzi.application.mission.MissionMemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.*; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_READY; @Slf4j @RequiredArgsConstructor @@ -23,10 +12,7 @@ @Component public class MissionReadyPushJob extends AbstractJob implements CustomAutomationJob { - private final MissionRepository missionRepository; - private final MissionMemberRepository missionMemberRepository; - - private final PushNotificationSender pushNotificationSender; + private final MissionMemberService missionMemberService; @Override protected ScheduleBuilder getScheduleBuilder() { @@ -36,25 +22,7 @@ protected ScheduleBuilder getScheduleBuilder() { } @Override - @Transactional protected void executeInternal(final JobExecutionContext context) throws JobExecutionException { - List missions = missionRepository.getReadyMissions(); - missions.forEach(mission -> { - if (mission.isReadyTime() && hasEnoughMember(mission.getId())) { - String topic = TopicGenerator.getTopic(mission.getId()); - pushNotificationSender.sendGroupMessage( - MISSION_READY.getTitle(), - MISSION_READY.getBody(), - topic - ); - } - }); - } - - private boolean hasEnoughMember(final Long missionId) { - List missionMembers = missionMemberRepository.findAllByMissionId(missionId); - int memberCount = missionMembers.size(); - - return memberCount >= MissionMemberCount.MIN.getCount(); + missionMemberService.sendReadyPushMessage(); } } diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java index 4811ec0..bc1b004 100644 --- a/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationPushJob.java @@ -1,24 +1,10 @@ package com.nexters.goalpanzi.schedule; -import com.nexters.goalpanzi.application.firebase.TopicGenerator; -import com.nexters.goalpanzi.domain.firebase.PushNotificationMessage; -import com.nexters.goalpanzi.domain.mission.Mission; -import com.nexters.goalpanzi.domain.mission.MissionVerification; -import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; -import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationRepository; -import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; +import com.nexters.goalpanzi.application.mission.MissionVerificationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.*; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; - -import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_NO_ONE_VERIFIED; -import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_VERIFIED; @Slf4j @RequiredArgsConstructor @@ -26,10 +12,7 @@ @Component public class MissionVerificationPushJob extends AbstractJob implements CustomAutomationJob { - private final MissionRepository missionRepository; - private final MissionVerificationRepository missionVerificationRepository; - - private final PushNotificationSender pushNotificationSender; + private final MissionVerificationService missionVerificationService; @Override protected ScheduleBuilder getScheduleBuilder() { @@ -39,40 +22,7 @@ protected ScheduleBuilder getScheduleBuilder() { } @Override - @Transactional protected void executeInternal(JobExecutionContext context) throws JobExecutionException { - LocalDate today = LocalDate.now(); - int hour = LocalDateTime.now().getHour(); - List missions = missionRepository.getInProgressMissions(); - - missions.forEach(mission -> { - if (mission.isMissionDay() && mission.isPushTime(hour)) { - List verifications = missionVerificationRepository.findAllByMissionIdAndDate(mission.getId(), today); - int verificationCount = verifications.size(); - String topic = TopicGenerator.getTopic(mission.getId()); - - if (verificationCount == 0) { - sendNoOneVerifiedMessage(MISSION_NO_ONE_VERIFIED, topic); - } else { - sendVerifiedMessage(MISSION_VERIFIED, topic, verificationCount); - } - } - }); - } - - private void sendVerifiedMessage(final PushNotificationMessage message, final String topic, final int verificationCount) { - pushNotificationSender.sendGroupMessage( - message.getTitle(verificationCount), - message.getBody(), - topic - ); - } - - private void sendNoOneVerifiedMessage(final PushNotificationMessage message, final String topic) { - pushNotificationSender.sendGroupMessage( - message.getTitle(), - message.getBody(), - topic - ); + missionVerificationService.sendVerificationPushMessage(); } } diff --git a/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java index 9570c35..ff2812d 100644 --- a/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java +++ b/src/main/java/com/nexters/goalpanzi/schedule/MissionVerificationWarningPushJob.java @@ -1,25 +1,10 @@ package com.nexters.goalpanzi.schedule; -import com.nexters.goalpanzi.domain.member.Member; -import com.nexters.goalpanzi.domain.mission.Mission; -import com.nexters.goalpanzi.domain.mission.MissionMember; -import com.nexters.goalpanzi.domain.mission.MissionVerification; -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.infrastructure.firebase.PushNotificationSender; +import com.nexters.goalpanzi.application.mission.MissionVerificationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.*; import org.springframework.stereotype.Component; -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.MISSION_VERIFICATION_WARNING; @Slf4j @RequiredArgsConstructor @@ -27,11 +12,7 @@ @Component public class MissionVerificationWarningPushJob extends AbstractJob implements CustomAutomationJob { - private final MissionRepository missionRepository; - private final MissionMemberRepository missionMemberRepository; - private final MissionVerificationRepository missionVerificationRepository; - - private final PushNotificationSender pushNotificationSender; + private final MissionVerificationService missionVerificationService; @Override protected ScheduleBuilder getScheduleBuilder() { @@ -41,32 +22,7 @@ protected ScheduleBuilder getScheduleBuilder() { } @Override - @Transactional protected void executeInternal(JobExecutionContext context) throws JobExecutionException { - LocalDate today = LocalDate.now(); - int hour = LocalDateTime.now().getHour(); - List missions = missionRepository.getInProgressMissions(); - - missions.forEach(mission -> { - if (mission.isMissionDay() && mission.isPushTime(hour)) { - List missionMembers = missionMemberRepository.findAllByMissionId(mission.getId()); - - missionMembers.forEach(missionMember -> { - Member member = missionMember.getMember(); - Optional verification = missionVerificationRepository.findByMemberIdAndMissionIdAndDate(member.getId(), mission.getId(), today); - if (verification.isEmpty() && member.getDeviceToken() != null) { - sendMessage(member); - } - }); - } - }); - } - - private void sendMessage(final Member member) { - pushNotificationSender.sendIndividualMessage( - MISSION_VERIFICATION_WARNING.getTitle(), - MISSION_VERIFICATION_WARNING.getBody(), - member.getDeviceToken() - ); + missionVerificationService.sendVerificationWarningPushMessage(); } } From b22174e11da3f499090474b0b0ebce79072e95c0 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Sun, 10 Nov 2024 20:16:15 +0900 Subject: [PATCH 12/16] =?UTF-8?q?remove:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B5=AC=EC=84=B1=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nexters/goalpanzi/config/AopConfig.java | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/main/java/com/nexters/goalpanzi/config/AopConfig.java diff --git a/src/main/java/com/nexters/goalpanzi/config/AopConfig.java b/src/main/java/com/nexters/goalpanzi/config/AopConfig.java deleted file mode 100644 index 8e8d5e7..0000000 --- a/src/main/java/com/nexters/goalpanzi/config/AopConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.nexters.goalpanzi.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; - -@Configuration -@EnableAspectJAutoProxy -public class AopConfig { -} From 505f338f0e27e1a44ad95ce11fc98e9c7082dd2e Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Sun, 10 Nov 2024 21:09:50 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20validator=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mission/MissionMemberService.java | 12 ++---------- .../application/mission/MissionValidator.java | 5 +++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java index f506913..c9d93a5 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java @@ -131,7 +131,7 @@ public void viewMissionRank(final Long missionId, final Long memberId) { public void sendReadyPushMessage() { List missions = missionRepository.getReadyMissions(); missions.forEach(mission -> { - if (mission.isReadyTime() && hasEnoughMember(mission.getId())) { + if (mission.isReadyTime() && missionValidator.hasEnoughMember(mission.getId())) { String topic = TopicGenerator.getTopic(mission.getId()); pushNotificationSender.sendGroupMessage( MISSION_READY.getTitle(), @@ -146,7 +146,7 @@ public void sendReadyPushMessage() { public void sendCancellationWarningPushMessage() { List missions = missionRepository.getReadyMissions(); missions.forEach(mission -> { - if (mission.isReadyTime() && !hasEnoughMember(mission.getId())) { + if (mission.isReadyTime() && !missionValidator.hasEnoughMember(mission.getId())) { String topic = TopicGenerator.getTopic(mission.getId()); pushNotificationSender.sendGroupMessage( MISSION_CANCELLATION_WARNING.getTitle(), @@ -156,12 +156,4 @@ public void sendCancellationWarningPushMessage() { } }); } - - private boolean hasEnoughMember(final Long missionId) { - List missionMembers = missionMemberRepository.findAllByMissionId(missionId); - int memberCount = missionMembers.size(); - - return memberCount >= MissionMemberCount.MIN.getCount(); - } - } diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionValidator.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionValidator.java index 4cdd4ca..a9d6712 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionValidator.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionValidator.java @@ -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 @@ -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(); } From 0f80d76495f1782ed2add731b6065179b1782209 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Sun, 10 Nov 2024 21:26:17 +0900 Subject: [PATCH 14/16] =?UTF-8?q?remove:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/MissionMemberCount.java | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java deleted file mode 100644 index 17d4b81..0000000 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionMemberCount.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.nexters.goalpanzi.domain.mission; - -import lombok.Getter; - -@Getter -public enum MissionMemberCount { - MIN(2); - - private final int count; - - MissionMemberCount(final int count) { - this.count = count; - } -} From 63e8bcfa34a9ae2268adfde0427c9b364f8fcab9 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Sun, 10 Nov 2024 21:31:00 +0900 Subject: [PATCH 15/16] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nexters/goalpanzi/domain/mission/MissionStatus.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java index ecdb102..d9066ca 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/MissionStatus.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; +import static com.nexters.goalpanzi.domain.mission.Mission.MIN_MISSION_MEMBER; import static com.nexters.goalpanzi.exception.ErrorCode.UNKNOWN_MISSION; @Getter @@ -44,11 +45,11 @@ public static MissionStatus fromMission( return CREATED; } - if (mission.isMissionPeriod() && currentMemberCount < MissionMemberCount.MIN.getCount()) { + if (mission.isMissionPeriod() && currentMemberCount < MIN_MISSION_MEMBER) { return CANCELED; } - if (mission.isMissionPeriod() && currentMemberCount >= MissionMemberCount.MIN.getCount()) { + if (mission.isMissionPeriod() && currentMemberCount >= MIN_MISSION_MEMBER) { return IN_PROGRESS; } From aa908dffddef60a3d080fdc9e56de85d607f2333 Mon Sep 17 00:00:00 2001 From: kimyu0218 Date: Sun, 10 Nov 2024 21:31:21 +0900 Subject: [PATCH 16/16] =?UTF-8?q?comment:=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nexters/goalpanzi/domain/mission/Mission.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java index 21a901e..671efd3 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java @@ -24,6 +24,7 @@ @Getter public class Mission extends BaseEntity { + public static final Integer MIN_MISSION_MEMBER = 2; public static final Integer MAX_MISSION_MEMBER = 10; @Id @@ -123,10 +124,12 @@ public boolean isMissionPeriod() { return !today.isBefore(missionStart) && !today.isAfter(missionEnd); } + // 오늘이 미션 인증 요일인지 검증 public boolean isMissionDay() { return this.missionDays.contains(DayOfWeek.valueOf(LocalDate.now().getDayOfWeek().name())); } + // 현재 시간이 미션 인증 시간인지 검증 public boolean isMissionTime() { String now = LocalTime.now().toString().substring(0, 5); return now.compareTo(uploadStartTime) >= 0 && now.compareTo(uploadEndTime) <= 0; @@ -149,6 +152,8 @@ public LocalDateTime getMissionUploadEndDateTime() { ); } + // 현재 시간이 미션 시작 예고 시간인지 검증 + // 미션 시작 예고 시간 == 미션 시작 1시간 전 public boolean isReadyTime() { LocalDateTime startTime = LocalDateTime.of(this.missionStartDate.toLocalDate(), LocalTime.parse(this.uploadStartTime)); Duration duration = Duration.between(startTime, LocalDate.now()); @@ -156,6 +161,9 @@ public boolean isReadyTime() { return duration.isNegative() && duration.toHours() <= 1; } + // 현재 시간이 푸시 시간인지 검증 + // 1. 인증 시간이 오전인 경우, 09시에 푸시 + // 2. 인증 시간이 오후이거나 종일인 경우, 15시에 푸시 public boolean isPushTime(final int hour) { if (this.uploadStartTime.equals(TimeOfDay.MORNING.getStartTime())) { return hour == PushTime.MORNING.getHour();