From 4a903b2b1c6c64ff58f7d6824155a36b1f3c1528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:36:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=9C=EA=B0=84=EC=9D=B4=20=EC=A7=80?= =?UTF-8?q?=EB=82=9C=20Alarm=EC=9D=80=20=EB=8B=A4=EC=8B=9C=20=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0=20=ED=95=B4=EB=8F=84=20=EC=95=88=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlarmDynamicRepositoryImpl.java | 14 +- .../subscription/entity/Subscription.java | 7 +- ..._add_subscription_last_alarm_toggle_at.sql | 2 + .../application/AlarmQueryServiceTest.java | 128 +++++++++++++----- 4 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 src/main/resources/db/migration/V4__add_subscription_last_alarm_toggle_at.sql diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java index c095b80..517942e 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java @@ -20,9 +20,17 @@ public class AlarmDynamicRepositoryImpl implements AlarmDynamicRepository { @Override public List findAlarms(Long userId, PageRequest pageRequest) { - return queryFactory.selectFrom(summary).innerJoin(summary.subscription, subscription).fetchJoin() - .where(subscription.user.id.eq(userId), subscription.isAlarmEnabled.eq(true)) - .orderBy(summary.updatedAt.desc(), summary.id.desc()).offset(pageRequest.page() * pageRequest.size()) + // 알람을 바꿨을 때 그 때를 기억해서 그 전에 목록은 안보이는게 맞다 + return queryFactory.selectFrom(summary) + .innerJoin(summary.subscription, subscription).fetchJoin() + .where( + subscription.user.id.eq(userId), + subscription.isAlarmEnabled.eq(true), + subscription.lastAlarmToggleAt.isNull() + .or(summary.createdAt.goe(subscription.lastAlarmToggleAt)) + ) + .orderBy(summary.updatedAt.desc(), summary.id.desc()) + .offset(pageRequest.page() * pageRequest.size()) .limit(pageRequest.size()).fetch(); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java index 8b1584f..098be93 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java @@ -44,6 +44,10 @@ public class Subscription extends BaseEntity { @Column(name = "last_seen_post_id", nullable = false) private String lastSeenPostId; + // 마지막으로 isAlarmEnabled를 수정한 순간 + @Column(name = "last_alarm_toggle_at", nullable = true) + private LocalDateTime lastAlarmToggleAt; + @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @@ -83,8 +87,9 @@ public void updateAlias(String alias) { } public void updateIsAlarmEnabled(Boolean alarmEnabled) { - if (alarmEnabled != null) { + if (alarmEnabled != null && this.isAlarmEnabled != alarmEnabled) { this.isAlarmEnabled = alarmEnabled; + this.lastAlarmToggleAt = LocalDateTime.now(); } } diff --git a/src/main/resources/db/migration/V4__add_subscription_last_alarm_toggle_at.sql b/src/main/resources/db/migration/V4__add_subscription_last_alarm_toggle_at.sql new file mode 100644 index 0000000..4805799 --- /dev/null +++ b/src/main/resources/db/migration/V4__add_subscription_last_alarm_toggle_at.sql @@ -0,0 +1,2 @@ +ALTER TABLE subscriptions + ADD COLUMN last_alarm_toggle_at datetime(6) NULL; diff --git a/src/test/java/com/todaysound/todaysound_server/query/alarm/application/AlarmQueryServiceTest.java b/src/test/java/com/todaysound/todaysound_server/query/alarm/application/AlarmQueryServiceTest.java index ca43579..14136e3 100644 --- a/src/test/java/com/todaysound/todaysound_server/query/alarm/application/AlarmQueryServiceTest.java +++ b/src/test/java/com/todaysound/todaysound_server/query/alarm/application/AlarmQueryServiceTest.java @@ -15,6 +15,7 @@ import com.todaysound.todaysound_server.domain.user.entity.UserType; import com.todaysound.todaysound_server.domain.user.repository.UserRepository; import com.todaysound.todaysound_server.global.dto.PageRequest; +import com.todaysound.todaysound_server.global.utils.CryptoUtils; import com.todaysound.todaysound_server.support.ServiceTestSupport; import java.time.LocalDateTime; import java.util.List; @@ -39,36 +40,101 @@ class AlarmQueryServiceTest extends ServiceTestSupport { @Autowired private AlarmQueryService alarmQueryService; -// @Test -// void 최근_알람을_조회한다() { -// // given -// Url url = Url.create("http://example.com", "Example Site"); -// urlRepository.save(url); -// -// User user = User.create("userId1", "hashedSecret", "fingerPrint", UserType.USER, true, "plainSecret"); -// userRepository.save(user); -// -// Subscription subscription = Subscription.create(url, true, "alias", user, "lastSeenPostId"); -// subscriptionRepository.save(subscription); -// -// Summary summary1 = Summary.create("hash1", "title1", "content1", "postUrl1", "postDate1", false, subscription); -// Summary summary2 = Summary.create("hash2", "title2", "content2", "postUrl2", "postDate2", false, subscription); -// Summary summary3 = Summary.create("hash3", "title3", "content3", "postUrl3", "postDate3", false, subscription); -// Summary summary4 = Summary.create("hash4", "title4", "content4", "postUrl4", "postDate4", false, subscription); -// ReflectionTestUtils.setField(summary4, "updatedAt", LocalDateTime.now().minusDays(1)); -// ReflectionTestUtils.setField(summary3, "updatedAt", LocalDateTime.now().minusDays(2)); -// ReflectionTestUtils.setField(summary2, "updatedAt", LocalDateTime.now().minusDays(3)); -// ReflectionTestUtils.setField(summary1, "updatedAt", LocalDateTime.now().minusDays(4)); -// summaryRepository.saveAll(List.of(summary1, summary2, summary3, summary4)); -// -// // when -// List result = alarmQueryService.getRecentAlarms(new PageRequest(0, 5), user.getUserId(), user.getHashedSecret()); -// -// // then -// assertThat(result.get(0).alias()).isEqualTo("title4"); -// assertThat(result.get(1).alias()).isEqualTo("title3"); -// assertThat(result.get(2).alias()).isEqualTo("title2"); -// assertThat(result.get(3).alias()).isEqualTo("title1"); -// } + @Test + void lastAlarmToggleAt_이전에_생성된_summary는_조회되지_않는다() { + // given + Url url = Url.create("http://example.com", "Example Site"); + urlRepository.save(url); + + String plainSecret = "plainSecret1"; + User user = User.create("userId1", "hashedSecret", CryptoUtils.sha256(plainSecret), UserType.USER, true, plainSecret); + userRepository.save(user); + + Subscription subscription = Subscription.create(url, true, "alias", user, "lastSeenPostId"); + LocalDateTime toggleAt = LocalDateTime.now(); + ReflectionTestUtils.setField(subscription, "lastAlarmToggleAt", toggleAt); + subscriptionRepository.save(subscription); + + // lastAlarmToggleAt 이전에 생성된 summary + Summary summaryBefore = Summary.create("hash1", "title1", "content1", "postUrl1", "postDate1", false, subscription); + ReflectionTestUtils.setField(summaryBefore, "createdAt", toggleAt.minusDays(1)); + ReflectionTestUtils.setField(summaryBefore, "updatedAt", toggleAt.minusDays(1)); + + // lastAlarmToggleAt 이후에 생성된 summary + Summary summaryAfter = Summary.create("hash2", "title2", "content2", "postUrl2", "postDate2", false, subscription); + ReflectionTestUtils.setField(summaryAfter, "createdAt", toggleAt.plusDays(1)); + ReflectionTestUtils.setField(summaryAfter, "updatedAt", toggleAt.plusDays(1)); + + summaryRepository.saveAll(List.of(summaryBefore, summaryAfter)); + + // when + List result = alarmQueryService.getRecentAlarms( + new PageRequest(0, 10), user.getUserId(), plainSecret); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).alias()).isEqualTo("title2"); + } + + @Test + void lastAlarmToggleAt이_null이면_모든_summary가_조회된다() { + // given + Url url = Url.create("http://example.com", "Example Site"); + urlRepository.save(url); + + String plainSecret = "plainSecret2"; + User user = User.create("userId2", "hashedSecret2", CryptoUtils.sha256(plainSecret), UserType.USER, true, plainSecret); + userRepository.save(user); + + Subscription subscription = Subscription.create(url, true, "alias", user, "lastSeenPostId"); + // lastAlarmToggleAt은 null (기본값) + subscriptionRepository.save(subscription); + + Summary summary1 = Summary.create("hash1", "title1", "content1", "postUrl1", "postDate1", false, subscription); + Summary summary2 = Summary.create("hash2", "title2", "content2", "postUrl2", "postDate2", false, subscription); + ReflectionTestUtils.setField(summary1, "createdAt", LocalDateTime.now().minusDays(10)); + ReflectionTestUtils.setField(summary1, "updatedAt", LocalDateTime.now().minusDays(10)); + ReflectionTestUtils.setField(summary2, "createdAt", LocalDateTime.now().minusDays(5)); + ReflectionTestUtils.setField(summary2, "updatedAt", LocalDateTime.now().minusDays(5)); + summaryRepository.saveAll(List.of(summary1, summary2)); + + // when + List result = alarmQueryService.getRecentAlarms( + new PageRequest(0, 10), user.getUserId(), plainSecret); + + // then + assertThat(result).hasSize(2); + } + + @Test + void 알람이_꺼져있는_subscription의_summary는_조회되지_않는다() { + // given + Url url = Url.create("http://example.com", "Example Site"); + urlRepository.save(url); + + String plainSecret = "plainSecret3"; + User user = User.create("userId3", "hashedSecret3", CryptoUtils.sha256(plainSecret), UserType.USER, true, plainSecret); + userRepository.save(user); + + // 알람이 꺼진 구독 + Subscription subscriptionOff = Subscription.create(url, false, "aliasOff", user, "lastSeenPostId"); + subscriptionRepository.save(subscriptionOff); + + // 알람이 켜진 구독 + Subscription subscriptionOn = Subscription.create(url, true, "aliasOn", user, "lastSeenPostId"); + subscriptionRepository.save(subscriptionOn); + + Summary summaryOff = Summary.create("hash1", "titleOff", "content1", "postUrl1", "postDate1", false, subscriptionOff); + Summary summaryOn = Summary.create("hash2", "titleOn", "content2", "postUrl2", "postDate2", false, subscriptionOn); + summaryRepository.saveAll(List.of(summaryOff, summaryOn)); + + // when + List result = alarmQueryService.getRecentAlarms( + new PageRequest(0, 10), user.getUserId(), plainSecret); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).alias()).isEqualTo("titleOn"); + } } \ No newline at end of file