Skip to content

Commit 23a2374

Browse files
authored
Merge pull request #321 from DevKor-github/fix/noti
fix: 고아 row들 제거하는 마이그레이션 작성, 방어적 백필 로직 추가
2 parents ac63312 + 18a5738 commit 23a2374

4 files changed

Lines changed: 138 additions & 18 deletions

File tree

src/main/java/org/devkor/apu/saerok_server/domain/notification/core/repository/NotificationSettingRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ public void save(NotificationSetting setting) {
7070
em.persist(setting);
7171
}
7272

73+
public void insertIfMissing(Long userDeviceId, NotificationType type, boolean enabled) {
74+
em.createNativeQuery("""
75+
insert into notification_setting (id, user_device_id, type, enabled, created_at, updated_at)
76+
values (nextval('notification_setting_seq'), :userDeviceId, :type, :enabled, now(), now())
77+
on conflict on constraint uq_notification_setting_user_device_type do nothing
78+
""")
79+
.setParameter("userDeviceId", userDeviceId)
80+
.setParameter("type", type.name())
81+
.setParameter("enabled", enabled)
82+
.executeUpdate();
83+
}
84+
7385
public int deleteByUserId(Long userId) {
7486
return em.createQuery("""
7587
delete from NotificationSetting ns
Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
package org.devkor.apu.saerok_server.domain.notification.core.service;
22

33
import lombok.RequiredArgsConstructor;
4-
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationSetting;
54
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationType;
65
import org.devkor.apu.saerok_server.domain.notification.core.entity.UserDevice;
76
import org.devkor.apu.saerok_server.domain.notification.core.repository.NotificationSettingRepository;
87
import org.springframework.stereotype.Service;
98
import org.springframework.transaction.annotation.Propagation;
109
import org.springframework.transaction.annotation.Transactional;
1110

12-
import java.util.EnumSet;
13-
import java.util.HashSet;
14-
import java.util.List;
15-
import java.util.Set;
16-
1711
@Service
1812
@RequiredArgsConstructor
1913
public class NotificationSettingBackfillService {
@@ -30,19 +24,13 @@ public void ensureDefaultsNewTx(UserDevice device) {
3024
/** 동일 트랜잭션 내 보정 */
3125
@Transactional
3226
public void ensureDefaults(UserDevice device) {
33-
List<NotificationSetting> existing = settingRepository.findByUserDeviceId(device.getId());
34-
35-
Set<NotificationType> have = new HashSet<>();
36-
for (NotificationSetting ns : existing) have.add(ns.getType());
37-
38-
Set<NotificationType> need = EnumSet.copyOf(schema.requiredTypes());
39-
need.removeAll(have);
27+
if (device == null || device.getId() == null) {
28+
throw new IllegalArgumentException("userDevice는 저장된 엔티티여야 합니다");
29+
}
4030

41-
if (!need.isEmpty()) {
42-
for (NotificationType t : need) {
43-
// 디폴트 on/off 정책: 기존 로직이 없으므로 기본 true로 시작(필요시 정책 변경)
44-
settingRepository.save(NotificationSetting.of(device, t, true));
45-
}
31+
for (NotificationType t : schema.requiredTypes()) {
32+
// 디폴트 on/off 정책: 기존 로직이 없으므로 기본 true로 시작(필요시 정책 변경)
33+
settingRepository.insertIfMissing(device.getId(), t, true);
4634
}
4735
}
4836
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
DELETE FROM notification_setting legacy
2+
WHERE legacy.type = 'SYSTEM_CONTENT_DELETED'
3+
AND EXISTS (
4+
SELECT 1
5+
FROM notification_setting current
6+
WHERE current.user_device_id = legacy.user_device_id
7+
AND current.type = 'SYSTEM_ADMIN_MESSAGE'
8+
);
9+
10+
UPDATE notification_setting
11+
SET type = 'SYSTEM_ADMIN_MESSAGE'
12+
WHERE type = 'SYSTEM_CONTENT_DELETED';
13+
14+
DELETE FROM notification_setting
15+
WHERE type NOT IN (
16+
'LIKED_ON_COLLECTION',
17+
'COMMENTED_ON_COLLECTION',
18+
'REPLIED_TO_COMMENT',
19+
'SUGGESTED_BIRD_ID_ON_COLLECTION',
20+
'COMMENTED_ON_FREE_BOARD_POST',
21+
'REPLIED_TO_FREE_BOARD_COMMENT',
22+
'SYSTEM_PUBLISHED_ANNOUNCEMENT',
23+
'SYSTEM_ADMIN_MESSAGE'
24+
);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.devkor.apu.saerok_server.domain.notification.application;
2+
3+
import org.devkor.apu.saerok_server.domain.notification.api.dto.response.NotificationSettingsResponse;
4+
import org.devkor.apu.saerok_server.domain.notification.core.entity.DevicePlatform;
5+
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationSetting;
6+
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationType;
7+
import org.devkor.apu.saerok_server.domain.notification.core.entity.UserDevice;
8+
import org.devkor.apu.saerok_server.domain.notification.core.repository.NotificationSettingRepository;
9+
import org.devkor.apu.saerok_server.domain.notification.core.repository.UserDeviceRepository;
10+
import org.devkor.apu.saerok_server.domain.notification.core.service.NotificationSettingBackfillService;
11+
import org.devkor.apu.saerok_server.domain.notification.core.service.NotificationTypeSchema;
12+
import org.devkor.apu.saerok_server.domain.notification.mapper.NotificationSettingWebMapper;
13+
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
14+
import org.devkor.apu.saerok_server.testsupport.AbstractPostgresContainerTest;
15+
import org.devkor.apu.saerok_server.testsupport.builder.UserBuilder;
16+
import org.junit.jupiter.api.DisplayName;
17+
import org.junit.jupiter.api.Test;
18+
import org.springframework.beans.factory.annotation.Autowired;
19+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
20+
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
21+
import org.springframework.context.annotation.Import;
22+
import org.springframework.test.context.ActiveProfiles;
23+
import org.springframework.transaction.PlatformTransactionManager;
24+
import org.springframework.transaction.annotation.Propagation;
25+
import org.springframework.transaction.annotation.Transactional;
26+
import org.springframework.transaction.support.TransactionTemplate;
27+
28+
import java.util.Arrays;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
@DataJpaTest
33+
@Import({
34+
NotificationSettingQueryService.class,
35+
NotificationSettingBackfillService.class,
36+
NotificationTypeSchema.class,
37+
NotificationSettingRepository.class,
38+
UserDeviceRepository.class,
39+
NotificationSettingWebMapper.class
40+
})
41+
@ActiveProfiles("test")
42+
class NotificationSettingQueryServiceTest extends AbstractPostgresContainerTest {
43+
44+
@Autowired TestEntityManager em;
45+
@Autowired NotificationSettingQueryService service;
46+
@Autowired UserDeviceRepository userDeviceRepository;
47+
@Autowired NotificationSettingRepository settingRepository;
48+
@Autowired PlatformTransactionManager transactionManager;
49+
50+
@Test
51+
@Transactional(propagation = Propagation.NOT_SUPPORTED)
52+
@DisplayName("설정 조회 시 누락된 자유게시판 알림 타입까지 기본 설정으로 백필한다")
53+
void getNotificationSettings_backfillsMissingTypes() {
54+
Long userId = new TransactionTemplate(transactionManager).execute(status -> {
55+
User user = new UserBuilder(em).build();
56+
UserDevice device = UserDevice.create(user, "device-1", "token-1", DevicePlatform.IOS);
57+
userDeviceRepository.save(device);
58+
userDeviceRepository.flush();
59+
60+
settingRepository.save(NotificationSetting.of(device, NotificationType.LIKED_ON_COLLECTION, true));
61+
em.flush();
62+
em.clear();
63+
return user.getId();
64+
});
65+
66+
try {
67+
NotificationSettingsResponse response =
68+
service.getNotificationSettings(userId, "device-1", DevicePlatform.IOS);
69+
NotificationSettingsResponse secondResponse =
70+
service.getNotificationSettings(userId, "device-1", DevicePlatform.IOS);
71+
72+
assertThat(response.items()).hasSize(NotificationType.values().length);
73+
assertThat(secondResponse.items()).hasSize(NotificationType.values().length);
74+
assertThat(response.items())
75+
.extracting(NotificationSettingsResponse.Item::type)
76+
.containsAll(Arrays.asList(NotificationType.values()));
77+
assertThat(response.items())
78+
.filteredOn(item -> item.type() == NotificationType.COMMENTED_ON_FREE_BOARD_POST
79+
|| item.type() == NotificationType.REPLIED_TO_FREE_BOARD_COMMENT)
80+
.extracting(NotificationSettingsResponse.Item::enabled)
81+
.containsOnly(true);
82+
} finally {
83+
new TransactionTemplate(transactionManager).execute(status -> {
84+
settingRepository.deleteByUserId(userId);
85+
userDeviceRepository.deleteByUserId(userId);
86+
87+
User user = em.find(User.class, userId);
88+
if (user != null) {
89+
em.remove(user);
90+
}
91+
em.flush();
92+
return null;
93+
});
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)