diff --git a/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/repository/NotificationSettingRepository.java b/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/repository/NotificationSettingRepository.java index 2865f948..d432a10e 100644 --- a/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/repository/NotificationSettingRepository.java +++ b/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/repository/NotificationSettingRepository.java @@ -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 diff --git a/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/service/NotificationSettingBackfillService.java b/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/service/NotificationSettingBackfillService.java index 5c55b29d..d07797cd 100644 --- a/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/service/NotificationSettingBackfillService.java +++ b/src/main/java/org/devkor/apu/saerok_server/domain/notification/core/service/NotificationSettingBackfillService.java @@ -1,7 +1,6 @@ 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; @@ -9,11 +8,6 @@ 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 { @@ -30,19 +24,13 @@ public void ensureDefaultsNewTx(UserDevice device) { /** 동일 트랜잭션 내 보정 */ @Transactional public void ensureDefaults(UserDevice device) { - List existing = settingRepository.findByUserDeviceId(device.getId()); - - Set have = new HashSet<>(); - for (NotificationSetting ns : existing) have.add(ns.getType()); - - Set 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); } } } diff --git a/src/main/resources/db/migration/V92__migrate_legacy_notification_setting_types.sql b/src/main/resources/db/migration/V92__migrate_legacy_notification_setting_types.sql new file mode 100644 index 00000000..ea5c7c40 --- /dev/null +++ b/src/main/resources/db/migration/V92__migrate_legacy_notification_setting_types.sql @@ -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' +); diff --git a/src/test/java/org/devkor/apu/saerok_server/domain/notification/application/NotificationSettingQueryServiceTest.java b/src/test/java/org/devkor/apu/saerok_server/domain/notification/application/NotificationSettingQueryServiceTest.java new file mode 100644 index 00000000..9ae946f0 --- /dev/null +++ b/src/test/java/org/devkor/apu/saerok_server/domain/notification/application/NotificationSettingQueryServiceTest.java @@ -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; + }); + } + } +}