From a31c4923295f0f08dc188d309890584314162a67 Mon Sep 17 00:00:00 2001 From: unifolio0 Date: Mon, 1 Dec 2025 17:13:23 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=AF=B8=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EC=99=84=EB=A3=8C=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A5=B4=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../debatetimer/config/SchedulerConfig.java | 10 +++ .../repository/poll/PollRepository.java | 13 +++- .../repository/poll/VoteRepository.java | 8 +- .../scheduler/PollCleanupScheduler.java | 29 ++++++++ .../scheduler/PollCleanupSchedulerTest.java | 74 +++++++++++++++++++ 6 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/debatetimer/config/SchedulerConfig.java create mode 100644 src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java create mode 100644 src/test/java/com/debatetimer/scheduler/PollCleanupSchedulerTest.java 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/repository/poll/PollRepository.java b/src/main/java/com/debatetimer/repository/poll/PollRepository.java index 22c6db56..ed38dd35 100644 --- a/src/main/java/com/debatetimer/repository/poll/PollRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/PollRepository.java @@ -1,15 +1,24 @@ 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.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.Repository; -public interface PollRepository extends JpaRepository { +public interface PollRepository extends Repository { + + PollEntity save(PollEntity pollEntity); + + Optional findById(long id); Optional findByIdAndMemberId(long id, long memberId); + List findAllByStatusAndCreatedAtBefore(PollStatus status, LocalDateTime createdAt); + default PollEntity getById(long id) { return findById(id) .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); 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..e1bd7768 --- /dev/null +++ b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java @@ -0,0 +1,29 @@ +package com.debatetimer.scheduler; + +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.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 PollRepository pollRepository; + + @Scheduled(fixedRate = INTERVAL_MILLIS) + @Transactional + public void cleanupStalePolls() { + LocalDateTime threshold = LocalDateTime.now().minusHours(TIMEOUT_HOURS); + pollRepository.findAllByStatusAndCreatedAtBefore(PollStatus.PROGRESS, threshold) + .forEach(PollEntity::updateToDone); + } +} 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); + } + } +} From e7bff5a6da46ed7e925b752f80a507a855ea445f Mon Sep 17 00:00:00 2001 From: unifolio0 Date: Tue, 2 Dec 2025 10:43:45 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EB=AA=A9=EB=A1=9D=EC=9C=BC=EB=A5=B4=20?= =?UTF-8?q?=EB=A9=94=EB=AA=A8=EB=A6=AC=EC=97=90=20=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20update=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EB=82=A0=EB=A6=AC=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../debatetimer/repository/poll/PollRepository.java | 10 +++++++--- .../debatetimer/scheduler/PollCleanupScheduler.java | 4 +--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/debatetimer/repository/poll/PollRepository.java b/src/main/java/com/debatetimer/repository/poll/PollRepository.java index ed38dd35..d4fddc6d 100644 --- a/src/main/java/com/debatetimer/repository/poll/PollRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/PollRepository.java @@ -5,9 +5,11 @@ import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; +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 Repository { @@ -17,8 +19,6 @@ public interface PollRepository extends Repository { Optional findByIdAndMemberId(long id, long memberId); - List findAllByStatusAndCreatedAtBefore(PollStatus status, LocalDateTime createdAt); - default PollEntity getById(long id) { return findById(id) .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); @@ -28,4 +28,8 @@ default PollEntity getByIdAndMemberId(long id, long memberId) { return findByIdAndMemberId(id, memberId) .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); } + + @Modifying(clearAutomatically = true) + @Query("UPDATE PollEntity p SET p.status = com.debatetimer.domain.poll.PollStatus.DONE WHERE p.status = :status AND p.createdAt <= :threshold") + void updateStatusToDoneForOldPolls(@Param("status") PollStatus status, @Param("threshold") LocalDateTime threshold); } diff --git a/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java index e1bd7768..ac839699 100644 --- a/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java +++ b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java @@ -1,7 +1,6 @@ package com.debatetimer.scheduler; 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; @@ -23,7 +22,6 @@ public class PollCleanupScheduler { @Transactional public void cleanupStalePolls() { LocalDateTime threshold = LocalDateTime.now().minusHours(TIMEOUT_HOURS); - pollRepository.findAllByStatusAndCreatedAtBefore(PollStatus.PROGRESS, threshold) - .forEach(PollEntity::updateToDone); + pollRepository.updateStatusToDoneForOldPolls(PollStatus.PROGRESS, threshold); } } From 4f1500af65ffdbf20075d771cc8fab110da49480 Mon Sep 17 00:00:00 2001 From: unifolio0 Date: Tue, 2 Dec 2025 13:10:20 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20FQCN=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/debatetimer/repository/poll/PollRepository.java | 5 +++-- .../java/com/debatetimer/scheduler/PollCleanupScheduler.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/debatetimer/repository/poll/PollRepository.java b/src/main/java/com/debatetimer/repository/poll/PollRepository.java index d4fddc6d..dd441755 100644 --- a/src/main/java/com/debatetimer/repository/poll/PollRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/PollRepository.java @@ -30,6 +30,7 @@ default PollEntity getByIdAndMemberId(long id, long memberId) { } @Modifying(clearAutomatically = true) - @Query("UPDATE PollEntity p SET p.status = com.debatetimer.domain.poll.PollStatus.DONE WHERE p.status = :status AND p.createdAt <= :threshold") - void updateStatusToDoneForOldPolls(@Param("status") PollStatus status, @Param("threshold") LocalDateTime threshold); + @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/scheduler/PollCleanupScheduler.java b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java index ac839699..b34ef2da 100644 --- a/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java +++ b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java @@ -22,6 +22,6 @@ public class PollCleanupScheduler { @Transactional public void cleanupStalePolls() { LocalDateTime threshold = LocalDateTime.now().minusHours(TIMEOUT_HOURS); - pollRepository.updateStatusToDoneForOldPolls(PollStatus.PROGRESS, threshold); + pollRepository.updateStatusToDoneForOldPolls(PollStatus.DONE, PollStatus.PROGRESS, threshold); } } From 0ab99efa56bde7a9a893fb60deec2fa3b077c79a Mon Sep 17 00:00:00 2001 From: unifolio0 Date: Tue, 2 Dec 2025 13:20:25 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domainrepository/poll/PollDomainRepository.java | 7 +++++++ .../com/debatetimer/scheduler/PollCleanupScheduler.java | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) 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/scheduler/PollCleanupScheduler.java b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java index b34ef2da..13253972 100644 --- a/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java +++ b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java @@ -1,7 +1,7 @@ package com.debatetimer.scheduler; import com.debatetimer.domain.poll.PollStatus; -import com.debatetimer.repository.poll.PollRepository; +import com.debatetimer.domainrepository.poll.PollDomainRepository; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; @@ -16,12 +16,12 @@ public class PollCleanupScheduler { private static final long INTERVAL_MILLIS = INTERVAL_HOURS * 60 * 60 * 1000L; static final int TIMEOUT_HOURS = 3; - private final PollRepository pollRepository; + private final PollDomainRepository pollDomainRepository; @Scheduled(fixedRate = INTERVAL_MILLIS) @Transactional public void cleanupStalePolls() { LocalDateTime threshold = LocalDateTime.now().minusHours(TIMEOUT_HOURS); - pollRepository.updateStatusToDoneForOldPolls(PollStatus.DONE, PollStatus.PROGRESS, threshold); + pollDomainRepository.updateStatusToDoneForOldPolls(PollStatus.PROGRESS, threshold); } } From 9110256261197353ade68446770843f89bffff96 Mon Sep 17 00:00:00 2001 From: unifolio0 Date: Wed, 3 Dec 2025 15:18:25 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/debatetimer/repository/poll/PollRepository.java | 2 +- .../java/com/debatetimer/scheduler/PollCleanupScheduler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/debatetimer/repository/poll/PollRepository.java b/src/main/java/com/debatetimer/repository/poll/PollRepository.java index dd441755..80b6624f 100644 --- a/src/main/java/com/debatetimer/repository/poll/PollRepository.java +++ b/src/main/java/com/debatetimer/repository/poll/PollRepository.java @@ -29,7 +29,7 @@ default PollEntity getByIdAndMemberId(long id, long memberId) { .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); } - @Modifying(clearAutomatically = true) + @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/scheduler/PollCleanupScheduler.java b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java index 13253972..9a88ba38 100644 --- a/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java +++ b/src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java @@ -18,7 +18,7 @@ public class PollCleanupScheduler { private final PollDomainRepository pollDomainRepository; - @Scheduled(fixedRate = INTERVAL_MILLIS) + @Scheduled(fixedRate = INTERVAL_MILLIS, zone = "Asia/Seoul") @Transactional public void cleanupStalePolls() { LocalDateTime threshold = LocalDateTime.now().minusHours(TIMEOUT_HOURS);