From 376e2bc2c4958006f915dc285a0d7032a0f8635d Mon Sep 17 00:00:00 2001 From: minsu20 Date: Thu, 4 Jan 2024 07:27:19 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20DAU=20=EC=8A=AC=EB=9E=99=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=B3=B4=EB=82=B4=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/MemberCustomRepository.java | 5 +- .../MemberCustomRepositoryImpl.java | 73 +++++++++++++++++++ .../domain/repository/MemberRepository.java | 3 - .../domain/service/MemberGetService.java | 6 ++ .../application/dto/DailyStats.java | 24 ++++++ .../service/DAUScheduleUseCase.java | 56 ++++++++++++++ .../exception/ExceptionEventHandler.java | 2 +- .../config/slack/team/TeamCreateHandler.java | 7 +- .../config/slack/util/SlackAdapter.java | 27 ++++++- .../global/config/slack/util/WebhookUtil.java | 3 + 10 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/moing/backend/domain/statistics/application/dto/DailyStats.java create mode 100644 src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java diff --git a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepository.java b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepository.java index 3f8ab192..20217552 100644 --- a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepository.java +++ b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepository.java @@ -1,14 +1,15 @@ package com.moing.backend.domain.member.domain.repository; import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.statistics.application.dto.DailyStats; import java.util.Optional; public interface MemberCustomRepository { - boolean checkNickname(String nickname); + boolean checkNickname(String nickname); Optional findNotDeletedBySocialId(String socialId); - Optional findNotDeletedByEmail(String email); Optional findNotDeletedByMemberId(Long id); + DailyStats getDailyStats(); } diff --git a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryImpl.java index 9c841321..e0d45867 100644 --- a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryImpl.java +++ b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryImpl.java @@ -1,12 +1,18 @@ package com.moing.backend.domain.member.domain.repository; import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.statistics.application.dto.DailyStats; import com.querydsl.jpa.impl.JPAQueryFactory; import javax.persistence.EntityManager; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Optional; import static com.moing.backend.domain.member.domain.entity.QMember.member; +import static com.moing.backend.domain.mission.domain.entity.QMission.mission; +import static com.moing.backend.domain.team.domain.entity.QTeam.team; public class MemberCustomRepositoryImpl implements MemberCustomRepository { @@ -52,4 +58,71 @@ public Optional findNotDeletedByMemberId(Long id) { .where(member.isDeleted.eq(false)) .fetchOne()); } + + @Override + public DailyStats getDailyStats() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long todayNewMembers = queryFactory + .selectFrom(member) + .where(member.createdDate.between(startOfToday, endOfToday)) + .fetchCount(); + + long yesterdayNewMembers = queryFactory + .selectFrom(member) + .where(member.createdDate.between(startOfYesterday, startOfToday)) + .fetchCount(); + + long todayNewTeams = queryFactory + .selectFrom(team) + .where(team.createdDate.between(startOfToday, endOfToday)) + .fetchCount(); + + long yesterdayNewTeams = queryFactory + .selectFrom(team) + .where(team.createdDate.between(startOfYesterday, startOfToday)) + .fetchCount(); + + long todayRepeatMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfToday, endOfToday) + .and(mission.type.eq(MissionType.REPEAT))) + .fetchCount(); + + long yesterdayRepeatMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfYesterday, startOfToday) + .and(mission.type.eq(MissionType.REPEAT))) + .fetchCount(); + + long todayOnceMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfToday, endOfToday) + .and(mission.type.eq(MissionType.ONCE))) + .fetchCount(); + + long yesterdayOnceMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfYesterday, startOfToday) + .and(mission.type.eq(MissionType.ONCE))) + .fetchCount(); + + return new DailyStats( + todayNewMembers, + yesterdayNewMembers, + todayNewTeams, + yesterdayNewTeams, + todayRepeatMissions, + yesterdayRepeatMissions, + todayOnceMissions, + yesterdayOnceMissions); + } + } diff --git a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberRepository.java b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberRepository.java index 4a236a75..8bc9886c 100644 --- a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberRepository.java @@ -2,8 +2,5 @@ import com.moing.backend.domain.member.domain.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - public interface MemberRepository extends JpaRepository, MemberCustomRepository { } diff --git a/src/main/java/com/moing/backend/domain/member/domain/service/MemberGetService.java b/src/main/java/com/moing/backend/domain/member/domain/service/MemberGetService.java index 8c78c8b8..027eb5b4 100644 --- a/src/main/java/com/moing/backend/domain/member/domain/service/MemberGetService.java +++ b/src/main/java/com/moing/backend/domain/member/domain/service/MemberGetService.java @@ -3,10 +3,12 @@ import com.moing.backend.domain.member.domain.entity.Member; import com.moing.backend.domain.member.domain.repository.MemberRepository; import com.moing.backend.domain.member.exception.NotFoundBySocialIdException; +import com.moing.backend.domain.statistics.application.dto.DailyStats; import com.moing.backend.global.annotation.DomainService; import lombok.RequiredArgsConstructor; import javax.transaction.Transactional; +import java.math.BigInteger; @DomainService @Transactional @@ -21,4 +23,8 @@ public Member getMemberBySocialId(String socialId){ public Member getMemberByMemberId(Long memberId) { return memberRepository.findNotDeletedByMemberId(memberId).orElseThrow(()->new NotFoundBySocialIdException()); } + + public DailyStats getDailyStats(){ + return memberRepository.getDailyStats(); + } } diff --git a/src/main/java/com/moing/backend/domain/statistics/application/dto/DailyStats.java b/src/main/java/com/moing/backend/domain/statistics/application/dto/DailyStats.java new file mode 100644 index 00000000..8c434da9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/dto/DailyStats.java @@ -0,0 +1,24 @@ +package com.moing.backend.domain.statistics.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.SqlResultSetMapping; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class DailyStats { + + private long todayNewMembers; + private long yesterdayNewMembers; + private long todayNewTeams; + private long yesterdayNewTeams; + private long todayRepeatMission; + private long yesterdayRepeatMission; + private long todayOnceMission; + private long yesterdayOnceMission; + +} + diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java new file mode 100644 index 00000000..e4189fce --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java @@ -0,0 +1,56 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.statistics.application.dto.DailyStats; +import com.moing.backend.global.config.slack.util.WebhookUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Slf4j +@Service +@Transactional +@EnableAsync +@EnableScheduling +@RequiredArgsConstructor +public class DAUScheduleUseCase { + + private final WebhookUtil webhookUtil; + private final MemberGetService memberGetService; + + public static final String DAILY_TEAM_CREATION_COUNT = "[DAU] 일일 모임 생성 수"; + public static final String DAILY_NEW_MEMBER_COUNT = "[DAU] 일일 신규 가입자 수"; + public static final String DAILY_REPEAT_MISSION_COUNT = "[DAU] 일일 반복 미션 생성 개수"; + public static final String DAILY_ONCE_MISSION_COUNT = "[DAU] 일일 한번 미션 생성 개수"; + + /* + DAU 정보 : 일일 모임 생성 수, 일일 신규 가입자 수, 일일 반복 미션 생성 수, 일일 한번 미션 생성 수 + */ + @Scheduled(cron = "0 59 23 * * *") + public void DailyTeamCreationInfoAlarm() { + Map todayStats = new LinkedHashMap<>(); + Map yesterdayStats = new LinkedHashMap<>(); + + DailyStats dailyStats = memberGetService.getDailyStats(); + todayStats.put(DAILY_TEAM_CREATION_COUNT, dailyStats.getTodayNewTeams()); + yesterdayStats.put(DAILY_TEAM_CREATION_COUNT, dailyStats.getYesterdayNewTeams()); + + todayStats.put(DAILY_NEW_MEMBER_COUNT, dailyStats.getTodayNewMembers()); + yesterdayStats.put(DAILY_NEW_MEMBER_COUNT, dailyStats.getYesterdayNewMembers()); + + todayStats.put(DAILY_REPEAT_MISSION_COUNT, dailyStats.getTodayRepeatMission()); + yesterdayStats.put(DAILY_REPEAT_MISSION_COUNT, dailyStats.getYesterdayRepeatMission()); + + todayStats.put(DAILY_ONCE_MISSION_COUNT, dailyStats.getTodayOnceMission()); + yesterdayStats.put(DAILY_ONCE_MISSION_COUNT, dailyStats.getYesterdayRepeatMission()); + webhookUtil.sendDailyStatsMessage(todayStats, yesterdayStats); + } + +} diff --git a/src/main/java/com/moing/backend/global/config/slack/exception/ExceptionEventHandler.java b/src/main/java/com/moing/backend/global/config/slack/exception/ExceptionEventHandler.java index 7466ba76..877decdc 100644 --- a/src/main/java/com/moing/backend/global/config/slack/exception/ExceptionEventHandler.java +++ b/src/main/java/com/moing/backend/global/config/slack/exception/ExceptionEventHandler.java @@ -13,7 +13,7 @@ public class ExceptionEventHandler { private final WebhookUtil webhookUtil; - @Async("asyncTaskExecutor") + @Async @EventListener public void onExceptionEvent(ExceptionEvent event) { webhookUtil.sendSlackAlertErrorLog(event.getRequest(), event.getException()); diff --git a/src/main/java/com/moing/backend/global/config/slack/team/TeamCreateHandler.java b/src/main/java/com/moing/backend/global/config/slack/team/TeamCreateHandler.java index 0985a3bc..34bcad01 100644 --- a/src/main/java/com/moing/backend/global/config/slack/team/TeamCreateHandler.java +++ b/src/main/java/com/moing/backend/global/config/slack/team/TeamCreateHandler.java @@ -3,10 +3,9 @@ import com.moing.backend.global.config.slack.team.dto.TeamCreateEvent; import com.moing.backend.global.config.slack.util.WebhookUtil; import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; @RequiredArgsConstructor @Component @@ -14,8 +13,8 @@ public class TeamCreateHandler { private final WebhookUtil webhookUtil; - @Async("asyncTaskExecutor") - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async + @EventListener public void onTeamCreateEvent(TeamCreateEvent event) { webhookUtil.sendSlackTeamCreatedMessage(event.getTeamName(), event.getLeaderId()); } diff --git a/src/main/java/com/moing/backend/global/config/slack/util/SlackAdapter.java b/src/main/java/com/moing/backend/global/config/slack/util/SlackAdapter.java index 2ce8f901..6272aeea 100644 --- a/src/main/java/com/moing/backend/global/config/slack/util/SlackAdapter.java +++ b/src/main/java/com/moing/backend/global/config/slack/util/SlackAdapter.java @@ -13,6 +13,8 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static com.slack.api.webhook.WebhookPayloads.payload; @@ -25,7 +27,7 @@ public class SlackAdapter implements WebhookUtil { private String errorWebhookUrl; @Value("${webhook.slack.team_alarm_url}") - private String teamAlarmWebhookUrl; + private String infoWebhookUrl; private final Slack slackClient = Slack.getInstance(); @@ -51,7 +53,7 @@ public void sendSlackAlertErrorLog(HttpServletRequest request, Exception e) { public void sendSlackTeamCreatedMessage(String teamName, Long leaderId) { String message = String.format("[새로운 소모임 '%s'이(가) 생성되었습니다.]", teamName); List attachments = List.of(generateSlackTeamAttachment(teamName, leaderId)); - sendSlackMessage(teamAlarmWebhookUrl, message, attachments); + sendSlackMessage(infoWebhookUrl, message, attachments); } private Attachment generateSlackTeamAttachment(String teamName, Long leaderId) { @@ -65,6 +67,27 @@ private Attachment generateSlackTeamAttachment(String teamName, Long leaderId) { .build(); } + @Override + public void sendDailyStatsMessage(Map todayStats, Map yesterdayStats) { + String message = "[일일 통계 알림]"; + List attachments = todayStats.keySet().stream() + .map(key -> generateDailyStatsAttachment(key, todayStats.get(key), yesterdayStats.getOrDefault(key, 0L))) + .collect(Collectors.toList()); + + sendSlackMessage(infoWebhookUrl, message, attachments); + } + + private Attachment generateDailyStatsAttachment(String title, long todayCount, long yesterdayCount) { + return Attachment.builder() + .color("1A66CC") // 색상 설정 + .title(title) + .fields(List.of( + generateSlackField("Today", String.valueOf(todayCount) + " 개"), + generateSlackField("Yesterday", String.valueOf(yesterdayCount) + " 개") + )) + .build(); + } + // attachment 생성 메서드 private Attachment generateSlackErrorAttachment(Exception e, HttpServletRequest request) { String requestTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").format(LocalDateTime.now()); diff --git a/src/main/java/com/moing/backend/global/config/slack/util/WebhookUtil.java b/src/main/java/com/moing/backend/global/config/slack/util/WebhookUtil.java index 7c613543..4f010e76 100644 --- a/src/main/java/com/moing/backend/global/config/slack/util/WebhookUtil.java +++ b/src/main/java/com/moing/backend/global/config/slack/util/WebhookUtil.java @@ -1,10 +1,13 @@ package com.moing.backend.global.config.slack.util; import javax.servlet.http.HttpServletRequest; +import java.util.Map; public interface WebhookUtil { void sendSlackAlertErrorLog(HttpServletRequest request, Exception e); void sendSlackTeamCreatedMessage(String teamName, Long leaderId); + + void sendDailyStatsMessage(Map todayStats, Map yesterdayStats); } From d3e33fc4903682f52b7c6eadac1805c1fe927d59 Mon Sep 17 00:00:00 2001 From: minsu20 Date: Thu, 4 Jan 2024 07:45:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=20=EC=9A=B4=EC=98=81=EC=84=9C=EB=B2=84=EC=97=90?= =?UTF-8?q?=EB=A7=8C=20=EC=A0=81=EC=9A=A9=EB=90=98=EA=B2=8C=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 --- .../domain/history/application/service/CleanupUseCase.java | 2 ++ .../application/service/MissionStateScheduleUseCase.java | 2 ++ .../statistics/application/service/DAUScheduleUseCase.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/main/java/com/moing/backend/domain/history/application/service/CleanupUseCase.java b/src/main/java/com/moing/backend/domain/history/application/service/CleanupUseCase.java index ed6d156c..5692f1c4 100644 --- a/src/main/java/com/moing/backend/domain/history/application/service/CleanupUseCase.java +++ b/src/main/java/com/moing/backend/domain/history/application/service/CleanupUseCase.java @@ -2,6 +2,7 @@ import com.moing.backend.domain.history.domain.service.AlarmHistoryDeleteService; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -11,6 +12,7 @@ @Service @RequiredArgsConstructor +@Profile("prod") public class CleanupUseCase { private final AlarmHistoryDeleteService alarmHistoryDeleteService; diff --git a/src/main/java/com/moing/backend/domain/missionState/application/service/MissionStateScheduleUseCase.java b/src/main/java/com/moing/backend/domain/missionState/application/service/MissionStateScheduleUseCase.java index a094b41f..f0d3bfd3 100644 --- a/src/main/java/com/moing/backend/domain/missionState/application/service/MissionStateScheduleUseCase.java +++ b/src/main/java/com/moing/backend/domain/missionState/application/service/MissionStateScheduleUseCase.java @@ -11,6 +11,7 @@ import com.moing.backend.domain.teamScore.application.service.TeamScoreLogicUseCase; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; @@ -26,6 +27,7 @@ @EnableAsync @EnableScheduling // 스케줄링 활성화 @RequiredArgsConstructor +@Profile("prod") public class MissionStateScheduleUseCase { private final MissionStateUseCase missionStateUseCase; diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java index e4189fce..c002f123 100644 --- a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java @@ -5,6 +5,7 @@ import com.moing.backend.global.config.slack.util.WebhookUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; @@ -20,6 +21,7 @@ @EnableAsync @EnableScheduling @RequiredArgsConstructor +@Profile("prod") public class DAUScheduleUseCase { private final WebhookUtil webhookUtil;