From 4385527e898fec37801d43e19625103e4a896168 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Wed, 18 Feb 2026 16:16:06 +0900 Subject: [PATCH] =?UTF-8?q?common:=20MissionCyclePolicy=EB=A5=BC=20common?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MissionCyclePolicy를 recommendation.service → common 패키지로 이동 (public) - toMissionDate(LocalDateTime) 메서드 추가 (임의 시각의 미션 날짜 계산) - getMissionCycleStart 내부를 toMissionDate 위임으로 리팩토링 - TeamActivityService의 중복 구현(getMissionDate, MISSION_RESET_TIME) 제거 - recommendation 서비스 3곳 import 업데이트 - RecommendationEmailService 파일 전체 들여쓰기 오염 수정 Closes #166 --- .../common/MissionCyclePolicy.java | 35 ++++ .../service/MissionCyclePolicy.java | 22 --- .../service/RecommendationEmailService.java | 151 +++++++++--------- .../service/RecommendationService.java | 1 + .../ScheduledRecommendationService.java | 1 + .../team/service/TeamActivityService.java | 18 +-- 6 files changed, 116 insertions(+), 112 deletions(-) create mode 100644 src/main/java/com/ryu/studyhelper/common/MissionCyclePolicy.java delete mode 100644 src/main/java/com/ryu/studyhelper/recommendation/service/MissionCyclePolicy.java diff --git a/src/main/java/com/ryu/studyhelper/common/MissionCyclePolicy.java b/src/main/java/com/ryu/studyhelper/common/MissionCyclePolicy.java new file mode 100644 index 0000000..0bdfe72 --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/common/MissionCyclePolicy.java @@ -0,0 +1,35 @@ +package com.ryu.studyhelper.common; + +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * 미션 사이클 도메인 정책 + * 매일 오전 6시를 기준으로 미션 사이클이 갱신된다. + */ +public class MissionCyclePolicy { + + public static final LocalTime MISSION_RESET_TIME = LocalTime.of(6, 0); + + /** + * 현재 미션 사이클의 시작 시각을 반환한다. + * 오전 6시 이전이면 전날 오전 6시를 반환한다. + */ + public static LocalDateTime getMissionCycleStart(Clock clock) { + LocalDateTime now = LocalDateTime.now(clock); + return toMissionDate(now).atTime(MISSION_RESET_TIME); + } + + /** + * 주어진 시각이 속하는 미션 날짜를 반환한다. + * 오전 6시 이전이면 전날로 취급한다. + */ + public static LocalDate toMissionDate(LocalDateTime dateTime) { + if (dateTime.toLocalTime().isBefore(MISSION_RESET_TIME)) { + return dateTime.toLocalDate().minusDays(1); + } + return dateTime.toLocalDate(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/recommendation/service/MissionCyclePolicy.java b/src/main/java/com/ryu/studyhelper/recommendation/service/MissionCyclePolicy.java deleted file mode 100644 index 21509d3..0000000 --- a/src/main/java/com/ryu/studyhelper/recommendation/service/MissionCyclePolicy.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ryu.studyhelper.recommendation.service; - -import java.time.Clock; -import java.time.LocalDateTime; -import java.time.LocalTime; - -/** - * 미션 사이클 도메인 정책 - * 매일 오전 6시를 기준으로 미션 사이클이 갱신된다. - */ -class MissionCyclePolicy { - - static final LocalTime MISSION_RESET_TIME = LocalTime.of(6, 0); - - static LocalDateTime getMissionCycleStart(Clock clock) { - LocalDateTime now = LocalDateTime.now(clock); - if (now.toLocalTime().isBefore(MISSION_RESET_TIME)) { - return now.toLocalDate().minusDays(1).atTime(MISSION_RESET_TIME); - } - return now.toLocalDate().atTime(MISSION_RESET_TIME); - } -} diff --git a/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java b/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java index e5a03d4..09e7271 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java @@ -1,94 +1,95 @@ - package com.ryu.studyhelper.recommendation.service; +package com.ryu.studyhelper.recommendation.service; - import com.ryu.studyhelper.recommendation.dto.internal.BatchResult; - import com.ryu.studyhelper.infrastructure.mail.sender.MailSender; - import com.ryu.studyhelper.recommendation.domain.member.EmailSendStatus; - import com.ryu.studyhelper.recommendation.domain.member.MemberRecommendation; - import com.ryu.studyhelper.recommendation.mailbuilder.RecommendationMailBuilder; - import com.ryu.studyhelper.recommendation.repository.MemberRecommendationRepository; - import lombok.RequiredArgsConstructor; - import lombok.extern.slf4j.Slf4j; - import org.springframework.stereotype.Service; - import org.springframework.transaction.annotation.Transactional; +import com.ryu.studyhelper.common.MissionCyclePolicy; +import com.ryu.studyhelper.recommendation.dto.internal.BatchResult; +import com.ryu.studyhelper.infrastructure.mail.sender.MailSender; +import com.ryu.studyhelper.recommendation.domain.member.EmailSendStatus; +import com.ryu.studyhelper.recommendation.domain.member.MemberRecommendation; +import com.ryu.studyhelper.recommendation.mailbuilder.RecommendationMailBuilder; +import com.ryu.studyhelper.recommendation.repository.MemberRecommendationRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; - import java.time.Clock; - import java.time.LocalDateTime; - import java.util.List; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; - /** - * 추천 이메일 발송 - * 배치(sendAll)와 수동(send) 모두 담당 - */ - @Service - @RequiredArgsConstructor - @Transactional - @Slf4j - public class RecommendationEmailService { - - private final Clock clock; - private final MailSender mailSender; - private final RecommendationMailBuilder recommendationMailBuilder; - private final MemberRecommendationRepository memberRecommendationRepository; +/** + * 추천 이메일 발송 + * 배치(sendAll)와 수동(send) 모두 담당 + */ +@Service +@RequiredArgsConstructor +@Transactional +@Slf4j +public class RecommendationEmailService { - /** - * 배치: PENDING 상태의 추천들에 대해 이메일 발송 - * 미션 사이클 기준(06:00~06:00)으로 조회 - */ - public BatchResult sendAll() { - LocalDateTime now = LocalDateTime.now(clock); - LocalDateTime missionCycleStart = MissionCyclePolicy.getMissionCycleStart(clock); - log.info("이메일 발송 배치 시작: {} (미션 사이클: {} 06:00 ~)", now.toLocalDate(), missionCycleStart.toLocalDate()); + private final Clock clock; + private final MailSender mailSender; + private final RecommendationMailBuilder recommendationMailBuilder; + private final MemberRecommendationRepository memberRecommendationRepository; - List pendingRecommendations = memberRecommendationRepository - .findPendingRecommendationsByCreatedAtBetween(missionCycleStart, now, EmailSendStatus.PENDING); - - int successCount = 0; - int failCount = 0; + /** + * 배치: PENDING 상태의 추천들에 대해 이메일 발송 + * 미션 사이클 기준(06:00~06:00)으로 조회 + */ + public BatchResult sendAll() { + LocalDateTime now = LocalDateTime.now(clock); + LocalDateTime missionCycleStart = MissionCyclePolicy.getMissionCycleStart(clock); + log.info("이메일 발송 배치 시작: {} (미션 사이클: {} 06:00 ~)", now.toLocalDate(), missionCycleStart.toLocalDate()); - for (MemberRecommendation mr : pendingRecommendations) { - if (sendEmail(mr)) { - successCount++; - } else { - failCount++; - } - } + List pendingRecommendations = memberRecommendationRepository + .findPendingRecommendationsByCreatedAtBetween(missionCycleStart, now, EmailSendStatus.PENDING); - log.info("이메일 발송 배치 완료 - 대상: {}개, 성공: {}개, 실패: {}개", - pendingRecommendations.size(), successCount, failCount); - return new BatchResult(pendingRecommendations.size(), successCount, failCount); - } + int successCount = 0; + int failCount = 0; - /** - * 수동 추천: 해당 추천의 팀원들에게 이메일 즉시 발송 - */ - public void send(List memberRecommendations) { - for (MemberRecommendation mr : memberRecommendations) { - sendEmail(mr); + for (MemberRecommendation mr : pendingRecommendations) { + if (sendEmail(mr)) { + successCount++; + } else { + failCount++; } } - private boolean sendEmail(MemberRecommendation mr) { - try { - String email = mr.getMember().getEmail(); - if (email == null || email.isBlank()) { - mr.markEmailAsFailed(); - memberRecommendationRepository.save(mr); - log.warn("회원 ID {}에 이메일이 없습니다", mr.getMember().getId()); - return false; - } - - mailSender.send(recommendationMailBuilder.build(mr)); + log.info("이메일 발송 배치 완료 - 대상: {}개, 성공: {}개, 실패: {}개", + pendingRecommendations.size(), successCount, failCount); + return new BatchResult(pendingRecommendations.size(), successCount, failCount); + } - mr.markEmailAsSent(); - memberRecommendationRepository.save(mr); - log.debug("회원 '{}' 이메일 발송 완료", mr.getMember().getHandle()); - return true; + /** + * 수동 추천: 해당 추천의 팀원들에게 이메일 즉시 발송 + */ + public void send(List memberRecommendations) { + for (MemberRecommendation mr : memberRecommendations) { + sendEmail(mr); + } + } - } catch (Exception e) { + private boolean sendEmail(MemberRecommendation mr) { + try { + String email = mr.getMember().getEmail(); + if (email == null || email.isBlank()) { mr.markEmailAsFailed(); memberRecommendationRepository.save(mr); - log.error("회원 ID {} 이메일 발송 실패", mr.getMember().getId(), e); + log.warn("회원 ID {}에 이메일이 없습니다", mr.getMember().getId()); return false; } + + mailSender.send(recommendationMailBuilder.build(mr)); + + mr.markEmailAsSent(); + memberRecommendationRepository.save(mr); + log.debug("회원 '{}' 이메일 발송 완료", mr.getMember().getHandle()); + return true; + + } catch (Exception e) { + mr.markEmailAsFailed(); + memberRecommendationRepository.save(mr); + log.error("회원 ID {} 이메일 발송 실패", mr.getMember().getId(), e); + return false; } } +} \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationService.java b/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationService.java index 48788dd..61582dd 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationService.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationService.java @@ -1,5 +1,6 @@ package com.ryu.studyhelper.recommendation.service; +import com.ryu.studyhelper.common.MissionCyclePolicy; import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; import com.ryu.studyhelper.problem.dto.projection.ProblemTagProjection; diff --git a/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java b/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java index 3eb613a..02d1a8c 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java @@ -1,5 +1,6 @@ package com.ryu.studyhelper.recommendation.service; +import com.ryu.studyhelper.common.MissionCyclePolicy; import com.ryu.studyhelper.recommendation.dto.internal.BatchResult; import com.ryu.studyhelper.recommendation.domain.RecommendationType; import com.ryu.studyhelper.recommendation.repository.RecommendationRepository; diff --git a/src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java b/src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java index 042d7bb..a597928 100644 --- a/src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java +++ b/src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java @@ -1,5 +1,6 @@ package com.ryu.studyhelper.team.service; +import com.ryu.studyhelper.common.MissionCyclePolicy; import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; import com.ryu.studyhelper.member.domain.Member; @@ -22,7 +23,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.*; import java.util.stream.Collectors; @@ -42,7 +42,6 @@ public class TeamActivityService { private static final int MAX_DAYS = 30; private static final int DEFAULT_DAYS = 30; - private static final LocalTime MISSION_RESET_TIME = LocalTime.of(6, 0); /** * 팀 활동 현황 조회 @@ -101,20 +100,9 @@ private void validatePrivateTeamAccess(Team team, Long memberId) { // ========== 기간 계산 ========== - /** - * 미션 사이클 기준 "오늘" 날짜 계산 - * 오전 6시 이전이면 전날로 취급 - */ - private LocalDate getMissionDate(LocalDateTime dateTime) { - if (dateTime.toLocalTime().isBefore(MISSION_RESET_TIME)) { - return dateTime.toLocalDate().minusDays(1); - } - return dateTime.toLocalDate(); - } - private QueryPeriod calculateQueryPeriod(Integer days) { int queryDays = calculateDays(days); - LocalDate endDate = getMissionDate(LocalDateTime.now()); + LocalDate endDate = MissionCyclePolicy.toMissionDate(LocalDateTime.now()); LocalDate startDate = endDate.minusDays(queryDays - 1); return QueryPeriod.of(queryDays, startDate, endDate); } @@ -229,7 +217,7 @@ private TeamActivityResponse.DailyActivity buildDailyActivity( List memberSolvedStatuses) { // 미션 사이클 기준 날짜 (6시 이전 생성분은 전날로 표시) - LocalDate date = getMissionDate(recommendation.getCreatedAt()); + LocalDate date = MissionCyclePolicy.toMissionDate(recommendation.getCreatedAt()); List problems = buildProblemInfoList(recommendation); List problemIds = problems.stream()