Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public List<ChallengeResponse> getChallenges(Member member, ChallengeQueryServic

@Transactional
public ChallengeResponse createChallenge(Member member, ChallengeCreateServiceRequest request,
LocalDateTime startDateTime) {
LocalDateTime startDateTime) {
// validation
categoryValidator.categoryExistsBy(request.getCategoryId());

Expand All @@ -72,8 +72,7 @@ public ChallengeResponse createChallenge(Member member, ChallengeCreateServiceRe
}

@Transactional
public ChallengeResponse achieveChallenge(Member member, Long challengeId,
ChallengeAchieveServiceRequest request) {
public ChallengeResponse achieveChallenge(Member member, Long challengeId, ChallengeAchieveServiceRequest request) {
// validation
validateChallengeAchieveOrCancel(member, challengeId, request.getAchieveDate());

Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/challenge/config/ClockConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.challenge.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Clock;

@Configuration
public class ClockConfig {

@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}

}
8 changes: 8 additions & 0 deletions src/main/java/com/challenge/domain/challenge/Challenge.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ public void addRecord(ChallengeRecord challengeRecord) {
this.challengeRecords.add(challengeRecord);
}

public void success() {
this.status = ChallengeStatus.SUCCEED;
}

public void expire() {
this.status = ChallengeStatus.EXPIRED;
}

public void delete() {
this.status = ChallengeStatus.REMOVED;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public boolean existsDuplicateRecordBy(Challenge challenge, LocalDate successDat
return count != null && count > 0;
}

// 진행중인 챌린지 조회
public List<Challenge> findOngoingChallengesBy() {
return queryFactory.selectFrom(challenge)
.where(challenge.status.eq(ChallengeStatus.ONGOING))
.fetch();
}

// 진행중인 챌린지 수 조회
public Long countOngoingChallengesBy(Member member) {
return queryFactory.select(challenge.count())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ChallengeRecordRepository extends JpaRepository<ChallengeRecord, Long> {

List<ChallengeRecord> findAllByChallengeId(Long challengeId);

}
78 changes: 78 additions & 0 deletions src/main/java/com/challenge/scheduler/ChallengeScheduler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.challenge.scheduler;

import com.challenge.domain.challenge.Challenge;
import com.challenge.domain.challenge.ChallengeQueryRepository;
import com.challenge.domain.challenge.ChallengeRepository;
import com.challenge.domain.challengeRecord.ChallengeRecord;
import com.challenge.domain.challengeRecord.ChallengeRecordRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.Clock;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Profile({"dev", "test"})
@Slf4j
@Component
@RequiredArgsConstructor
public class ChallengeScheduler {

private final Clock clock;

private final ChallengeQueryRepository challengeQueryRepository;

private final ChallengeRepository challengeRepository;
private final ChallengeRecordRepository challengeRecordRepository;

/**
* 챌린지 상태 업데이트 스케줄러 - 매일 00시 00분 00초 실행
*/
@Scheduled(cron = "0 0 0 * * *")
public void updateChallengeStatus() {
LocalDateTime now = LocalDateTime.now(clock);

// '진행 중' 상태의 챌린지 가져오기
List<Challenge> ongoingChallenges = challengeQueryRepository.findOngoingChallengesBy();

for (Challenge challenge : ongoingChallenges) {
// 종료 시간이 현재 시간보다 이후인 경우 상태 업데이트를 건너뜀
if (challenge.getEndDateTime().isAfter(now)) {
continue;
}

// 챌린지 기록에서 해당 챌린지 ID에 대한 기록 가져오기
List<ChallengeRecord> records = challengeRecordRepository.findAllByChallengeId(challenge.getId());

// 날짜별로 마지막 상태가 '달성'인 기록 카운트
long successCount = calculateSuccessCount(records);

// 상태 변경: 목표 횟수 이상이면 성공, 아니면 기간 만료
if (successCount >= challenge.getTotalGoalCount()) {
challenge.success();
} else {
challenge.expire();
}

// 변경된 상태 저장
challengeRepository.save(challenge);
}
}

private long calculateSuccessCount(List<ChallengeRecord> records) {
return records.stream()
.collect(Collectors.groupingBy(ChallengeRecord::getRecordDate, Collectors.toList()))
.values().stream()
.filter(recordList -> {
// 해당 날짜의 마지막 기록이 '달성' 상태인지 확인
ChallengeRecord lastRecord = recordList.get(recordList.size() - 1);
return lastRecord.isSucceed();
})
.count();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,33 @@ void existsDuplicateRecordBy() {
assertThat(result).isTrue();
}

@DisplayName("진행중인 챌린지를 조회한다.")
@Test
void findOngoingChallengesBy() {
// given
Member member = createMember();
memberRepository.save(member);

Category category = createCategory();
categoryRepository.save(category);

Challenge challenge1 = createChallenge(member, category, 1, "제목1", ONGOING,
LocalDateTime.of(2024, 10, 1, 12, 30, 59));
Challenge challenge2 = createChallenge(member, category, 2, "제목2", ONGOING,
LocalDateTime.of(2024, 11, 11, 14, 0, 0));
Challenge challenge3 = createChallenge(member, category, 3, "제목3", SUCCEED,
LocalDateTime.of(2024, 12, 23, 0, 0, 0));
Challenge challenge4 = createChallenge(member, category, 1, "제목4", REMOVED,
LocalDateTime.of(2025, 1, 1, 0, 0, 0));
challengeRepository.saveAll(List.of(challenge1, challenge2, challenge3, challenge4));

// when
List<Challenge> ongoingChallenges = challengeQueryRepository.findOngoingChallengesBy();

// then
assertThat(ongoingChallenges).hasSize(2);
}

@DisplayName("진행중인 챌린지 수를 조회한다.")
@Test
void countOngoingChallenges() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.challenge.domain.challengeRecord;

import com.challenge.domain.category.Category;
import com.challenge.domain.category.CategoryRepository;
import com.challenge.domain.challenge.Challenge;
import com.challenge.domain.challenge.ChallengeRepository;
import com.challenge.domain.challenge.ChallengeStatus;
import com.challenge.domain.job.Job;
import com.challenge.domain.job.JobRepository;
import com.challenge.domain.member.Gender;
import com.challenge.domain.member.JobYear;
import com.challenge.domain.member.LoginType;
import com.challenge.domain.member.Member;
import com.challenge.domain.member.MemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import static com.challenge.domain.challenge.ChallengeStatus.ONGOING;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;

@SpringBootTest
@ActiveProfiles("test")
class ChallengeRecordRepositoryTest {

@Autowired
private ChallengeRepository challengeRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private CategoryRepository categoryRepository;

@Autowired
private JobRepository jobRepository;

@Autowired
private ChallengeRecordRepository challengeRecordRepository;

@AfterEach
void tearDown() {
challengeRecordRepository.deleteAllInBatch();
challengeRepository.deleteAllInBatch();
memberRepository.deleteAllInBatch();
categoryRepository.deleteAllInBatch();
jobRepository.deleteAllInBatch();
}

@DisplayName("특정 챌린지 ID에 대한 모든 챌린지 기록을 조회한다.")
@Test
void findAllByChallengeId() {
// given
Category category = createCategory();
categoryRepository.save(category);

Member member = createMember();
memberRepository.save(member);

Challenge challenge1 = createChallenge(member, category, 1, "제목1", ONGOING,
LocalDateTime.of(2024, 10, 1, 12, 30, 59));
Challenge challenge2 = createChallenge(member, category, 2, "제목2", ONGOING,
LocalDateTime.of(2024, 11, 16, 14, 0, 0));
Challenge challenge3 = createChallenge(member, category, 3, "제목3", ONGOING,
LocalDateTime.of(2024, 12, 1, 0, 0, 0));
challengeRepository.saveAll(List.of(challenge1, challenge2, challenge3));

LocalDate currentDate = LocalDate.of(2025, 1, 18);
ChallengeRecord challengeRecord1 = createRecord(challenge1, currentDate, true);
ChallengeRecord challengeRecord2 = createRecord(challenge1, currentDate, false);
ChallengeRecord challengeRecord3 = createRecord(challenge1, currentDate, true);
challengeRecordRepository.saveAll(List.of(challengeRecord1, challengeRecord2, challengeRecord3));

// when
List<ChallengeRecord> challengeRecords = challengeRecordRepository.findAllByChallengeId(challenge1.getId());

// then
assertThat(challengeRecords).hasSize(3)
.extracting("recordDate", "isSucceed")
.containsExactlyInAnyOrder(
tuple(currentDate, true),
tuple(currentDate, false),
tuple(currentDate, true)
);
}

private Member createMember() {
Job job = Job.builder()
.code("1")
.description("1")
.build();
jobRepository.save(job);

return Member.builder()
.socialId(1L)
.email("eamil")
.loginType(LoginType.KAKAO)
.nickname("nickname")
.birth(LocalDate.of(2000, 1, 1))
.gender(Gender.MALE)
.jobYear(JobYear.LT_1Y)
.job(job)
.build();
}

private Challenge createChallenge(Member member, Category category, int durationInWeeks, String title,
ChallengeStatus status, LocalDateTime startDateTime) {
return Challenge.builder()
.member(member)
.category(category)
.durationInWeeks(durationInWeeks)
.title(title)
.color("#30B0C7")
.status(status)
.weeklyGoalCount(1)
.startDateTime(startDateTime)
.build();
}

private Category createCategory() {
return Category.builder()
.name("카테고리")
.build();
}

private ChallengeRecord createRecord(Challenge challenge, LocalDate currentDate, boolean isSucceed) {
return ChallengeRecord.builder()
.challenge(challenge)
.recordDate(currentDate)
.isSucceed(isSucceed)
.build();
}

}
Loading
Loading