Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public void save(NotificationSetting setting) {
em.persist(setting);
}

public void insertIfMissing(Long userDeviceId, NotificationType type, boolean enabled) {
em.createNativeQuery("""
insert into notification_setting (id, user_device_id, type, enabled, created_at, updated_at)
values (nextval('notification_setting_seq'), :userDeviceId, :type, :enabled, now(), now())
on conflict on constraint uq_notification_setting_user_device_type do nothing
""")
.setParameter("userDeviceId", userDeviceId)
.setParameter("type", type.name())
.setParameter("enabled", enabled)
.executeUpdate();
}

public int deleteByUserId(Long userId) {
return em.createQuery("""
delete from NotificationSetting ns
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
package org.devkor.apu.saerok_server.domain.notification.core.service;

import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationSetting;
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationType;
import org.devkor.apu.saerok_server.domain.notification.core.entity.UserDevice;
import org.devkor.apu.saerok_server.domain.notification.core.repository.NotificationSettingRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Service
@RequiredArgsConstructor
public class NotificationSettingBackfillService {
Expand All @@ -30,19 +24,13 @@ public void ensureDefaultsNewTx(UserDevice device) {
/** 동일 트랜잭션 내 보정 */
@Transactional
public void ensureDefaults(UserDevice device) {
List<NotificationSetting> existing = settingRepository.findByUserDeviceId(device.getId());

Set<NotificationType> have = new HashSet<>();
for (NotificationSetting ns : existing) have.add(ns.getType());

Set<NotificationType> need = EnumSet.copyOf(schema.requiredTypes());
need.removeAll(have);
if (device == null || device.getId() == null) {
throw new IllegalArgumentException("userDevice는 저장된 엔티티여야 합니다");
}

if (!need.isEmpty()) {
for (NotificationType t : need) {
// 디폴트 on/off 정책: 기존 로직이 없으므로 기본 true로 시작(필요시 정책 변경)
settingRepository.save(NotificationSetting.of(device, t, true));
}
for (NotificationType t : schema.requiredTypes()) {
// 디폴트 on/off 정책: 기존 로직이 없으므로 기본 true로 시작(필요시 정책 변경)
settingRepository.insertIfMissing(device.getId(), t, true);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
DELETE FROM notification_setting legacy
WHERE legacy.type = 'SYSTEM_CONTENT_DELETED'
AND EXISTS (
SELECT 1
FROM notification_setting current
WHERE current.user_device_id = legacy.user_device_id
AND current.type = 'SYSTEM_ADMIN_MESSAGE'
);

UPDATE notification_setting
SET type = 'SYSTEM_ADMIN_MESSAGE'
WHERE type = 'SYSTEM_CONTENT_DELETED';

DELETE FROM notification_setting
WHERE type NOT IN (
'LIKED_ON_COLLECTION',
'COMMENTED_ON_COLLECTION',
'REPLIED_TO_COMMENT',
'SUGGESTED_BIRD_ID_ON_COLLECTION',
'COMMENTED_ON_FREE_BOARD_POST',
'REPLIED_TO_FREE_BOARD_COMMENT',
'SYSTEM_PUBLISHED_ANNOUNCEMENT',
'SYSTEM_ADMIN_MESSAGE'
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.devkor.apu.saerok_server.domain.notification.application;

import org.devkor.apu.saerok_server.domain.notification.api.dto.response.NotificationSettingsResponse;
import org.devkor.apu.saerok_server.domain.notification.core.entity.DevicePlatform;
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationSetting;
import org.devkor.apu.saerok_server.domain.notification.core.entity.NotificationType;
import org.devkor.apu.saerok_server.domain.notification.core.entity.UserDevice;
import org.devkor.apu.saerok_server.domain.notification.core.repository.NotificationSettingRepository;
import org.devkor.apu.saerok_server.domain.notification.core.repository.UserDeviceRepository;
import org.devkor.apu.saerok_server.domain.notification.core.service.NotificationSettingBackfillService;
import org.devkor.apu.saerok_server.domain.notification.core.service.NotificationTypeSchema;
import org.devkor.apu.saerok_server.domain.notification.mapper.NotificationSettingWebMapper;
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
import org.devkor.apu.saerok_server.testsupport.AbstractPostgresContainerTest;
import org.devkor.apu.saerok_server.testsupport.builder.UserBuilder;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@Import({
NotificationSettingQueryService.class,
NotificationSettingBackfillService.class,
NotificationTypeSchema.class,
NotificationSettingRepository.class,
UserDeviceRepository.class,
NotificationSettingWebMapper.class
})
@ActiveProfiles("test")
class NotificationSettingQueryServiceTest extends AbstractPostgresContainerTest {

@Autowired TestEntityManager em;
@Autowired NotificationSettingQueryService service;
@Autowired UserDeviceRepository userDeviceRepository;
@Autowired NotificationSettingRepository settingRepository;
@Autowired PlatformTransactionManager transactionManager;

@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@DisplayName("설정 조회 시 누락된 자유게시판 알림 타입까지 기본 설정으로 백필한다")
void getNotificationSettings_backfillsMissingTypes() {
Long userId = new TransactionTemplate(transactionManager).execute(status -> {
User user = new UserBuilder(em).build();
UserDevice device = UserDevice.create(user, "device-1", "token-1", DevicePlatform.IOS);
userDeviceRepository.save(device);
userDeviceRepository.flush();

settingRepository.save(NotificationSetting.of(device, NotificationType.LIKED_ON_COLLECTION, true));
em.flush();
em.clear();
return user.getId();
});

try {
NotificationSettingsResponse response =
service.getNotificationSettings(userId, "device-1", DevicePlatform.IOS);
NotificationSettingsResponse secondResponse =
service.getNotificationSettings(userId, "device-1", DevicePlatform.IOS);

assertThat(response.items()).hasSize(NotificationType.values().length);
assertThat(secondResponse.items()).hasSize(NotificationType.values().length);
assertThat(response.items())
.extracting(NotificationSettingsResponse.Item::type)
.containsAll(Arrays.asList(NotificationType.values()));
assertThat(response.items())
.filteredOn(item -> item.type() == NotificationType.COMMENTED_ON_FREE_BOARD_POST
|| item.type() == NotificationType.REPLIED_TO_FREE_BOARD_COMMENT)
.extracting(NotificationSettingsResponse.Item::enabled)
.containsOnly(true);
} finally {
new TransactionTemplate(transactionManager).execute(status -> {
settingRepository.deleteByUserId(userId);
userDeviceRepository.deleteByUserId(userId);

User user = em.find(User.class, userId);
if (user != null) {
em.remove(user);
}
em.flush();
return null;
});
}
}
}
Loading