diff --git a/.gitignore b/.gitignore index 671e0e9f..7d690374 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ out/ ### application-local.yml /src/main/resources/application-local.yml +.serena diff --git a/src/main/java/com/debatetimer/config/SchedulerConfig.java b/src/main/java/com/debatetimer/config/SchedulerConfig.java new file mode 100644 index 00000000..49893796 --- /dev/null +++ b/src/main/java/com/debatetimer/config/SchedulerConfig.java @@ -0,0 +1,10 @@ +package com.debatetimer.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulerConfig { + +} diff --git a/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java index e35ceca4..4bf45775 100644 --- a/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java @@ -1,8 +1,10 @@ package com.debatetimer.domainrepository.poll; import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.PollStatus; import com.debatetimer.entity.poll.PollEntity; import com.debatetimer.repository.poll.PollRepository; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -38,4 +40,9 @@ public Poll finishPoll(long pollId, long memberId) { pollEntity.updateToDone(); return pollEntity.toDomain(); } + + @Transactional + public void updateStatusToDoneForOldPolls(PollStatus pollStatus, LocalDateTime threshold) { + pollRepository.updateStatusToDoneForOldPolls(PollStatus.DONE, pollStatus, threshold); + } } diff --git a/src/main/java/com/debatetimer/repository/poll/PollRepository.java b/src/main/java/com/debatetimer/repository/poll/PollRepository.java index 22c6db56..80b6624f 100644 --- a/src/main/java/com/debatetimer/repository/poll/PollRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/PollRepository.java @@ -1,12 +1,21 @@ package com.debatetimer.repository.poll; +import com.debatetimer.domain.poll.PollStatus; import com.debatetimer.entity.poll.PollEntity; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.time.LocalDateTime; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; -public interface PollRepository extends JpaRepository { +public interface PollRepository extends Repository { + + PollEntity save(PollEntity pollEntity); + + Optional findById(long id); Optional findByIdAndMemberId(long id, long memberId); @@ -19,4 +28,9 @@ default PollEntity getByIdAndMemberId(long id, long memberId) { return findByIdAndMemberId(id, memberId) .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); } + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE PollEntity p SET p.status = :doneStatus WHERE p.status = :status AND p.createdAt <= :threshold") + void updateStatusToDoneForOldPolls(@Param("doneStatus") PollStatus doneStatus, @Param("status") PollStatus status, + @Param("threshold") LocalDateTime threshold); } diff --git a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java index 5f211efd..9cf73a36 100644 --- a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java @@ -2,11 +2,15 @@ import com.debatetimer.entity.poll.VoteEntity; import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.Repository; -public interface VoteRepository extends JpaRepository { +public interface VoteRepository extends Repository { + + VoteEntity save(VoteEntity voteEntity); List findAllByPollId(long pollId); boolean existsByPollIdAndParticipateCode(long pollId, String participateCode); + + long count(); } diff --git a/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java new file mode 100644 index 00000000..9a88ba38 --- /dev/null +++ b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java @@ -0,0 +1,27 @@ +package com.debatetimer.scheduler; + +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domainrepository.poll.PollDomainRepository; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class PollCleanupScheduler { + + private static final int INTERVAL_HOURS = 12; + private static final long INTERVAL_MILLIS = INTERVAL_HOURS * 60 * 60 * 1000L; + static final int TIMEOUT_HOURS = 3; + + private final PollDomainRepository pollDomainRepository; + + @Scheduled(fixedRate = INTERVAL_MILLIS, zone = "Asia/Seoul") + @Transactional + public void cleanupStalePolls() { + LocalDateTime threshold = LocalDateTime.now().minusHours(TIMEOUT_HOURS); + pollDomainRepository.updateStatusToDoneForOldPolls(PollStatus.PROGRESS, threshold); + } +} diff --git a/src/test/java/com/debatetimer/scheduler/PollCleanupSchedulerTest.java b/src/test/java/com/debatetimer/scheduler/PollCleanupSchedulerTest.java new file mode 100644 index 00000000..cea12f4d --- /dev/null +++ b/src/test/java/com/debatetimer/scheduler/PollCleanupSchedulerTest.java @@ -0,0 +1,74 @@ +package com.debatetimer.scheduler; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.repository.poll.PollRepository; +import com.debatetimer.service.BaseServiceTest; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; + +class PollCleanupSchedulerTest extends BaseServiceTest { + + @Autowired + private PollRepository pollRepository; + + @Autowired + private PollCleanupScheduler pollCleanupScheduler; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Nested + class CleanupStalePolls { + + @Test + void 생성_후_일정_시간_이상_경과한_진행_상태인_투표를_완료_상태로_변경한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity poll = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + updateCreatedAt(poll.getId(), LocalDateTime.now().minusHours(PollCleanupScheduler.TIMEOUT_HOURS + 1)); + + pollCleanupScheduler.cleanupStalePolls(); + + PollStatus status = pollRepository.getById(poll.getId()).getStatus(); + assertThat(status).isEqualTo(PollStatus.DONE); + } + + @Test + void 생성_후_일정_시간_미만_경과한_진행_상태인_투표는_그대로_유지한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity poll = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + updateCreatedAt(poll.getId(), LocalDateTime.now().minusHours(PollCleanupScheduler.TIMEOUT_HOURS - 1)); + + pollCleanupScheduler.cleanupStalePolls(); + + PollStatus status = pollRepository.getById(poll.getId()).getStatus(); + assertThat(status).isEqualTo(PollStatus.PROGRESS); + } + + @Test + void 이미_완료_상태인_투표는_영향받지_않는다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity poll = pollEntityGenerator.generate(table, PollStatus.DONE); + updateCreatedAt(poll.getId(), LocalDateTime.now().minusHours(PollCleanupScheduler.TIMEOUT_HOURS + 1)); + + pollCleanupScheduler.cleanupStalePolls(); + + PollStatus status = pollRepository.getById(poll.getId()).getStatus(); + assertThat(status).isEqualTo(PollStatus.DONE); + } + + private void updateCreatedAt(Long pollId, LocalDateTime createdAt) { + jdbcTemplate.update("UPDATE poll SET created_at = ? WHERE id = ?", createdAt, pollId); + } + } +}