diff --git a/src/main/java/BE_Elixir/Elixir/domain/auth/service/AuthService.java b/src/main/java/BE_Elixir/Elixir/domain/auth/service/AuthService.java index dab1281..296a104 100644 --- a/src/main/java/BE_Elixir/Elixir/domain/auth/service/AuthService.java +++ b/src/main/java/BE_Elixir/Elixir/domain/auth/service/AuthService.java @@ -3,6 +3,7 @@ import BE_Elixir.Elixir.domain.auth.dto.AccessTokenDTO; import BE_Elixir.Elixir.domain.auth.dto.response.TokenResponseDTO; import BE_Elixir.Elixir.domain.auth.dto.request.LoginRequestDTO; +import BE_Elixir.Elixir.domain.challenge.service.ChallengeAchievementService; import BE_Elixir.Elixir.domain.member.entity.MemberDetails; import BE_Elixir.Elixir.domain.member.service.MemberDetailsService; import BE_Elixir.Elixir.global.exception.CustomException; @@ -30,6 +31,7 @@ public class AuthService { private final JwtProvider jwtProvider; private final RedisAuthService redisAuthService; private final MemberDetailsService memberDetailsService; + private final ChallengeAchievementService challengeAchievementService; // 로그인 (jwt 발급 및 Redis 저장) public TokenResponseDTO signIn(LoginRequestDTO request) { @@ -51,6 +53,11 @@ public TokenResponseDTO signIn(LoginRequestDTO request) { redisAuthService.saveRefreshToken(email, refreshToken); log.info("Refresh Token Redis에 저장: email={}, token={}", email, refreshToken); + String memberEmail = request.getEmail(); + + // 자동 참여 메서드 호출 + challengeAchievementService.challengeParticipation(memberEmail); + return tokenResponse; } catch (BadCredentialsException e) { log.warn("로그인 실패 - 잘못된 비밀번호: {}", request.getEmail()); diff --git a/src/main/java/BE_Elixir/Elixir/domain/challenge/entity/ChallengeAchievementId.java b/src/main/java/BE_Elixir/Elixir/domain/challenge/entity/ChallengeAchievementId.java index 83788f0..7754a1d 100644 --- a/src/main/java/BE_Elixir/Elixir/domain/challenge/entity/ChallengeAchievementId.java +++ b/src/main/java/BE_Elixir/Elixir/domain/challenge/entity/ChallengeAchievementId.java @@ -12,6 +12,11 @@ public class ChallengeAchievementId implements Serializable { private Long memberId; private Long challengeId; + public ChallengeAchievementId(Long memberId, Long challengeId) { + this.memberId = memberId; + this.challengeId = challengeId; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/BE_Elixir/Elixir/domain/challenge/event/listener/ChallengeEventListener.java b/src/main/java/BE_Elixir/Elixir/domain/challenge/event/listener/ChallengeEventListener.java index 759cbb3..d59aee3 100644 --- a/src/main/java/BE_Elixir/Elixir/domain/challenge/event/listener/ChallengeEventListener.java +++ b/src/main/java/BE_Elixir/Elixir/domain/challenge/event/listener/ChallengeEventListener.java @@ -21,7 +21,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -119,6 +121,7 @@ else if (event instanceof RecipeEvent) { handleGoal(goalType, memberId, openedAt, resultSetter); } + // 목표 달성 결과에 따라 다음 단계 활성화 if (achievement.isStep1Goal1Achieved() && achievement.isStep1Goal2Achieved()) { achievement.setStep2Goal1Active(true); @@ -168,10 +171,28 @@ private void handleGoal(ChallengeGoalType goalType, Long memberId, LocalDateTime case DIET_LUNCH -> achieved = dietLogRepository.existsByMemberIdAndTypeAndTimeAfter(memberId, DietLogType.점심, openedAt); case DIET_THREE_MEALS -> { - boolean hasBreakfast = dietLogRepository.existsByMemberIdAndTypeAndTimeAfter(memberId, DietLogType.아침, openedAt); - boolean hasLunch = dietLogRepository.existsByMemberIdAndTypeAndTimeAfter(memberId, DietLogType.점심, openedAt); - boolean hasDinner = dietLogRepository.existsByMemberIdAndTypeAndTimeAfter(memberId, DietLogType.저녁, openedAt); - achieved = hasBreakfast && hasLunch && hasDinner; + // 특정 날짜 단위로 아침/점심/저녁이 모두 기록된 날이 있는지를 체크 + List datesWithBreakfast = dietLogRepository.findTimesWithDietTypeAfter(memberId, DietLogType.아침, openedAt).stream() + .map(LocalDateTime::toLocalDate) + .distinct() + .toList(); + + List datesWithLunch = dietLogRepository.findTimesWithDietTypeAfter(memberId, DietLogType.점심, openedAt).stream() + .map(LocalDateTime::toLocalDate) + .distinct() + .toList(); + + List datesWithDinner = dietLogRepository.findTimesWithDietTypeAfter(memberId, DietLogType.저녁, openedAt).stream() + .map(LocalDateTime::toLocalDate) + .distinct() + .toList(); + + // 교집합으로 하루라도 3끼 다 먹은 날이 있는지 확인 + Set breakfastSet = new HashSet<>(datesWithBreakfast); + breakfastSet.retainAll(datesWithLunch); + breakfastSet.retainAll(datesWithDinner); + + achieved = !breakfastSet.isEmpty(); } case DIET_SEASONAL_ONCE -> { // 사용자의 식단에 포함된 식재료 목록 diff --git a/src/main/java/BE_Elixir/Elixir/domain/challenge/repository/ChallengeAchievementRepository.java b/src/main/java/BE_Elixir/Elixir/domain/challenge/repository/ChallengeAchievementRepository.java index 515d029..80ac90d 100644 --- a/src/main/java/BE_Elixir/Elixir/domain/challenge/repository/ChallengeAchievementRepository.java +++ b/src/main/java/BE_Elixir/Elixir/domain/challenge/repository/ChallengeAchievementRepository.java @@ -1,12 +1,13 @@ package BE_Elixir.Elixir.domain.challenge.repository; import BE_Elixir.Elixir.domain.challenge.entity.ChallengeAchievement; +import BE_Elixir.Elixir.domain.challenge.entity.ChallengeAchievementId; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Optional; -public interface ChallengeAchievementRepository extends JpaRepository { +public interface ChallengeAchievementRepository extends JpaRepository { // 챌린지에 대한 사용자의 달성 상태 정보 조회 Optional findByChallengeIdAndMemberId(Long challengeId, Long memberId); diff --git a/src/main/java/BE_Elixir/Elixir/domain/challenge/service/ChallengeAchievementService.java b/src/main/java/BE_Elixir/Elixir/domain/challenge/service/ChallengeAchievementService.java index cc0d983..f585818 100644 --- a/src/main/java/BE_Elixir/Elixir/domain/challenge/service/ChallengeAchievementService.java +++ b/src/main/java/BE_Elixir/Elixir/domain/challenge/service/ChallengeAchievementService.java @@ -4,8 +4,10 @@ import BE_Elixir.Elixir.domain.challenge.dto.response.ChallengeProgressResponseDTO; import BE_Elixir.Elixir.domain.challenge.entity.Challenge; import BE_Elixir.Elixir.domain.challenge.entity.ChallengeAchievement; +import BE_Elixir.Elixir.domain.challenge.entity.ChallengeAchievementId; import BE_Elixir.Elixir.domain.challenge.repository.ChallengeAchievementRepository; import BE_Elixir.Elixir.domain.challenge.repository.ChallengeRepository; +import BE_Elixir.Elixir.domain.member.repository.MemberRepository; import BE_Elixir.Elixir.global.exception.CustomException; import BE_Elixir.Elixir.global.exception.ErrorCode; import jakarta.transaction.Transactional; @@ -22,6 +24,7 @@ public class ChallengeAchievementService { private final ChallengeRepository challengeRepository; private final ChallengeAchievementRepository challengeAchievementRepository; + private final MemberRepository memberRepository; @Transactional public void save(ChallengeAchievement achievement) { @@ -120,4 +123,40 @@ public ChallengeProgressResponseDTO getBeforeProgress(Long memberId, Long challe .orElseGet(() -> ChallengeProgressResponseDTO.empty(challenge)); } + // 챌린지 자동 참여 + @Transactional + public void challengeParticipation(String memberEmail) { + LocalDate now = LocalDate.now(); + int year = now.getYear(); + int month = now.getMonthValue(); + + // 회원 정보 조회 (email -> memberId) + Long memberId = memberRepository.findByEmail(memberEmail) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)) + .getId(); + + Challenge challenge = challengeRepository.findByYearAndMonth(year, month) + .orElseThrow(() -> new CustomException(ErrorCode.CHALLENGE_NOT_FOUND)); + + ChallengeAchievementId id = new ChallengeAchievementId(memberId, challenge.getId()); + + boolean exists = challengeAchievementRepository.existsById(id); + + if (!exists) { + ChallengeAchievement achievement = new ChallengeAchievement(); + achievement.setMemberId(memberId); + achievement.setChallengeId(challenge.getId()); + achievement.setStep1Goal1Active(true); + achievement.setStep1Goal2Active(true); + achievement.setStep2Goal1Active(false); + achievement.setStep2Goal2Active(false); + achievement.setStep3Goal1Active(false); + achievement.setStep3Goal2Active(false); + achievement.setStep4Goal1Active(false); + achievement.setStep4Goal2Active(false); + achievement.setOpenedAt(LocalDateTime.now()); + + challengeAchievementRepository.save(achievement); + } + } } diff --git a/src/main/java/BE_Elixir/Elixir/domain/dietLog/repository/DietLogRepository.java b/src/main/java/BE_Elixir/Elixir/domain/dietLog/repository/DietLogRepository.java index b2d6367..d8573b1 100644 --- a/src/main/java/BE_Elixir/Elixir/domain/dietLog/repository/DietLogRepository.java +++ b/src/main/java/BE_Elixir/Elixir/domain/dietLog/repository/DietLogRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -46,5 +47,9 @@ int countDietLogsInMonth(@Param("memberId") Long memberId, @Param("startOfMonth") LocalDateTime startOfMonth, @Param("endOfMonth") LocalDateTime endOfMonth); - + // - 특정 시간 이후에 작성된 기록 + @Query("SELECT DISTINCT dl.time FROM DietLog dl WHERE dl.member.id = :memberId AND dl.type = :type AND dl.time > :after") + List findTimesWithDietTypeAfter(@Param("memberId") Long memberId, + @Param("type") DietLogType type, + @Param("after") LocalDateTime after); } \ No newline at end of file