From 07b6b983afe6c3ce0d1aad3fd619d9f5066184ab Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:02:30 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9/=EB=B3=B8=EB=AC=B8=20=EC=88=98=EC=A0=95=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp/picke/domain/notification/entity/Notification.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java b/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java index e360140..6efda0e 100644 --- a/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java +++ b/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java @@ -68,6 +68,11 @@ private Notification(User user, NotificationCategory category, NotificationDetai this.read = false; } + public void updateContent(String title, String body) { + this.title = title; + this.body = body; + } + public void markAsRead() { if (!this.read) { this.read = true; From 7b0ac90a7891e77adad8162bf6e182b1f57cee7e Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:02:34 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=20DTO=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/request/AdminNoticeUpdateRequest.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/swyp/picke/domain/admin/dto/notification/request/AdminNoticeUpdateRequest.java diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/notification/request/AdminNoticeUpdateRequest.java b/src/main/java/com/swyp/picke/domain/admin/dto/notification/request/AdminNoticeUpdateRequest.java new file mode 100644 index 0000000..8def0bb --- /dev/null +++ b/src/main/java/com/swyp/picke/domain/admin/dto/notification/request/AdminNoticeUpdateRequest.java @@ -0,0 +1,8 @@ +package com.swyp.picke.domain.admin.dto.notification.request; + +import jakarta.validation.constraints.NotBlank; + +public record AdminNoticeUpdateRequest( + @NotBlank String title, + @NotBlank String body +) {} From 49b65f91407dbccf4ead0b6a194a09b96894d685 Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:02:38 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AdminNotificationService.java | 16 ++++ .../service/AdminNotificationServiceTest.java | 92 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java diff --git a/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java b/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java index c5f6605..9ad0af9 100644 --- a/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java +++ b/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java @@ -1,6 +1,7 @@ package com.swyp.picke.domain.admin.service; import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeCreateRequest; +import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeUpdateRequest; import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeDetailResponse; import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeListResponse; import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeSummaryResponse; @@ -71,6 +72,21 @@ public AdminNoticeDetailResponse getNoticeDetail(Long notificationId) { return toDetailResponse(notification); } + @Transactional + public AdminNoticeDetailResponse updateNotice(Long notificationId, AdminNoticeUpdateRequest request) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); + notification.updateContent(request.title(), request.body()); + return toDetailResponse(notification); + } + + @Transactional + public void deleteNotice(Long notificationId) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); + notificationRepository.delete(notification); + } + private NotificationCategory normalizeCategory(NotificationCategory category) { if (category == null || category == NotificationCategory.ALL) { return null; diff --git a/src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java b/src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java new file mode 100644 index 0000000..b815356 --- /dev/null +++ b/src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java @@ -0,0 +1,92 @@ +package com.swyp.picke.domain.admin.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeUpdateRequest; +import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeDetailResponse; +import com.swyp.picke.domain.notification.entity.Notification; +import com.swyp.picke.domain.notification.enums.NotificationCategory; +import com.swyp.picke.domain.notification.enums.NotificationDetailCode; +import com.swyp.picke.domain.notification.repository.NotificationRepository; +import com.swyp.picke.domain.notification.service.NotificationDispatchService; +import com.swyp.picke.domain.notification.service.NotificationService; +import com.swyp.picke.global.common.exception.CustomException; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AdminNotificationServiceTest { + + @Mock + private NotificationService notificationService; + + @Mock + private NotificationDispatchService notificationDispatchService; + + @Mock + private NotificationRepository notificationRepository; + + @InjectMocks + private AdminNotificationService adminNotificationService; + + private Notification newNotice(String title, String body) { + return Notification.builder() + .user(null) + .category(NotificationCategory.NOTICE) + .detailCode(NotificationDetailCode.POLICY_CHANGE) + .title(title) + .body(body) + .build(); + } + + @Test + @DisplayName("공지사항을 수정한다") + void updateNotice_updatesExistingNotice() { + Notification notification = newNotice("원본 제목", "원본 본문"); + when(notificationRepository.findById(1L)).thenReturn(Optional.of(notification)); + + AdminNoticeDetailResponse response = adminNotificationService.updateNotice( + 1L, new AdminNoticeUpdateRequest("수정된 제목", "수정된 본문")); + + assertThat(response.title()).isEqualTo("수정된 제목"); + assertThat(response.body()).isEqualTo("수정된 본문"); + } + + @Test + @DisplayName("존재하지 않는 공지사항을 수정하면 예외를 던진다") + void updateNotice_throws_whenNotFound() { + when(notificationRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> adminNotificationService.updateNotice( + 999L, new AdminNoticeUpdateRequest("제목", "본문"))) + .isInstanceOf(CustomException.class); + } + + @Test + @DisplayName("공지사항을 삭제한다") + void deleteNotice_removesExistingNotice() { + Notification notification = newNotice("제목", "본문"); + when(notificationRepository.findById(1L)).thenReturn(Optional.of(notification)); + + adminNotificationService.deleteNotice(1L); + + verify(notificationRepository).delete(notification); + } + + @Test + @DisplayName("존재하지 않는 공지사항을 삭제하면 예외를 던진다") + void deleteNotice_throws_whenNotFound() { + when(notificationRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> adminNotificationService.deleteNotice(999L)) + .isInstanceOf(CustomException.class); + } +} From ce9c9fed25616d231b5cbab4643cb4fe67484231 Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:02:42 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20API=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminNotificationController.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/com/swyp/picke/domain/admin/controller/AdminNotificationController.java b/src/main/java/com/swyp/picke/domain/admin/controller/AdminNotificationController.java index c434a9c..2154814 100644 --- a/src/main/java/com/swyp/picke/domain/admin/controller/AdminNotificationController.java +++ b/src/main/java/com/swyp/picke/domain/admin/controller/AdminNotificationController.java @@ -1,6 +1,7 @@ package com.swyp.picke.domain.admin.controller; import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeCreateRequest; +import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeUpdateRequest; import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationTestRequest; import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeDetailResponse; import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeListResponse; @@ -13,9 +14,11 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -55,6 +58,22 @@ public ApiResponse getNoticeDetail(@PathVariable Long return ApiResponse.onSuccess(adminNotificationService.getNoticeDetail(noticeId)); } + @Operation(summary = "공지사항 수정", description = "이미 발송된 알림함/푸시는 재발송되지 않으며, 알림함에 남는 텍스트만 갱신된다.") + @PutMapping("/{noticeId}") + public ApiResponse updateNotice( + @PathVariable Long noticeId, + @RequestBody @Valid AdminNoticeUpdateRequest request + ) { + return ApiResponse.onSuccess(adminNotificationService.updateNotice(noticeId, request)); + } + + @Operation(summary = "공지사항 삭제") + @DeleteMapping("/{noticeId}") + public ApiResponse deleteNotice(@PathVariable Long noticeId) { + adminNotificationService.deleteNotice(noticeId); + return ApiResponse.onSuccess(null); + } + @Operation(summary = "푸시 알림 발송 테스트", description = "특정 유저의 등록된 디바이스로 알림 설정(ON/OFF) 무관하게 즉시 테스트 푸시를 발송한다.") @PostMapping("/test") public ApiResponse sendTestPush( From facc5054a1297f8b8b736fc4431a3bcbd04771ef Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:02:46 +0900 Subject: [PATCH 5/7] =?UTF-8?q?docs:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20API=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api-specs/notification-api.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/api-specs/notification-api.md b/docs/api-specs/notification-api.md index 1e6309c..59c7a4f 100644 --- a/docs/api-specs/notification-api.md +++ b/docs/api-specs/notification-api.md @@ -347,6 +347,8 @@ | `POST` | `/api/v1/admin/notices` | 공지사항/이벤트 작성 (즉시 전체 발송) | | `GET` | `/api/v1/admin/notices` | 공지 목록 조회 (`category`, `page`, `size` 쿼리) | | `GET` | `/api/v1/admin/notices/{noticeId}` | 공지 상세 조회 | +| `PUT` | `/api/v1/admin/notices/{noticeId}` | 공지 수정 (제목/본문) | +| `DELETE` | `/api/v1/admin/notices/{noticeId}` | 공지 삭제 | | `POST` | `/api/v1/admin/notices/test` | 특정 유저 대상 테스트 푸시 발송 (알림 설정 ON/OFF 무관) | **`POST /api/v1/admin/notices` 요청 바디** @@ -367,6 +369,26 @@ 응답은 `AdminNoticeDetailResponse` (`notificationId`, `category`, `detailCode`, `title`, `body`, `referenceId`, `createdAt`). +**`PUT /api/v1/admin/notices/{noticeId}` 요청 바디** + +```json +{ + "title": "서비스 점검 안내 (수정)", + "body": "8/30 02:00~05:00로 점검 시간이 변경되었습니다." +} +``` + +| 필드 | 타입 | 필수 | 설명 | +|---|---|---|---| +| `title` | `string` | Y | 알림 제목 | +| `body` | `string` | Y | 알림 본문 | + +`category`/`detailCode`는 등록 시점에 확정되어 수정 대상이 아닙니다. 이미 발송된 알림함/푸시는 재발송되지 않고, 알림함에 남아있는 텍스트만 갱신됩니다. 응답은 등록 API와 동일한 `AdminNoticeDetailResponse`. + +**`DELETE /api/v1/admin/notices/{noticeId}`** + +성공 시 `200 OK`, `data: null`. 삭제된 공지는 알림함에서도 사라집니다. + **`POST /api/v1/admin/notices/test` 요청 바디** ```json From 77ba6a1052f9d792c63e0a42ccc70ddb111ea2d8 Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:06:51 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20soft=20delete?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20deletedAt=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/entity/Notification.java | 7 +++++++ .../repository/NotificationRepository.java | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java b/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java index 6efda0e..7a773de 100644 --- a/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java +++ b/src/main/java/com/swyp/picke/domain/notification/entity/Notification.java @@ -55,6 +55,9 @@ public class Notification extends BaseEntity { @Column(name = "read_at") private LocalDateTime readAt; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder private Notification(User user, NotificationCategory category, NotificationDetailCode detailCode, String title, String body, Long referenceId, Long perspectiveId) { @@ -79,4 +82,8 @@ public void markAsRead() { this.readAt = LocalDateTime.now(); } } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/swyp/picke/domain/notification/repository/NotificationRepository.java b/src/main/java/com/swyp/picke/domain/notification/repository/NotificationRepository.java index 14bf3bd..129bd0d 100644 --- a/src/main/java/com/swyp/picke/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/swyp/picke/domain/notification/repository/NotificationRepository.java @@ -2,6 +2,7 @@ import com.swyp.picke.domain.notification.entity.Notification; import com.swyp.picke.domain.notification.enums.NotificationCategory; +import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,12 +12,15 @@ public interface NotificationRepository extends JpaRepository { + Optional findByIdAndDeletedAtIsNull(Long id); + @Query(""" SELECT n FROM Notification n WHERE ( (n.user IS NOT NULL AND n.user.id = :userId) OR n.user IS NULL ) + AND n.deletedAt IS NULL AND (:category IS NULL OR n.category = :category) ORDER BY n.createdAt DESC """) @@ -30,6 +34,7 @@ Slice findVisibleNotifications( SELECT CASE WHEN COUNT(n) > 0 THEN true ELSE false END FROM Notification n WHERE n.user IS NULL + AND n.deletedAt IS NULL AND n.category = :category AND NOT EXISTS ( SELECT 1 FROM NotificationRead nr @@ -41,7 +46,8 @@ AND NOT EXISTS ( @Query(""" SELECT CASE WHEN COUNT(n) > 0 THEN true ELSE false END FROM Notification n - WHERE (:category IS NULL OR n.category = :category) + WHERE n.deletedAt IS NULL + AND (:category IS NULL OR n.category = :category) AND ( (n.user.id = :userId AND n.read = false) OR (n.user IS NULL AND NOT EXISTS ( @@ -61,7 +67,8 @@ SELECT CASE WHEN COUNT(n) > 0 THEN true ELSE false END @Query(""" SELECT n FROM Notification n - WHERE (:category IS NULL OR n.category = :category) + WHERE n.deletedAt IS NULL + AND (:category IS NULL OR n.category = :category) ORDER BY n.createdAt DESC """) Slice findNotificationsForAdmin( From 4bbcd8891d0a49ea7f187b5aa68c8c80986c539f Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:06:56 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20hard=20delete=20=EB=8C=80=EC=8B=A0=20soft?= =?UTF-8?q?=20delete=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/service/AdminNotificationService.java | 8 ++++---- .../notification/service/NotificationService.java | 2 +- .../service/AdminNotificationServiceTest.java | 15 +++++++-------- .../service/NotificationServiceTest.java | 10 +++++----- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java b/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java index 9ad0af9..19da48c 100644 --- a/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java +++ b/src/main/java/com/swyp/picke/domain/admin/service/AdminNotificationService.java @@ -67,14 +67,14 @@ public AdminNoticeListResponse getNotices(NotificationCategory category, int pag } public AdminNoticeDetailResponse getNoticeDetail(Long notificationId) { - Notification notification = notificationRepository.findById(notificationId) + Notification notification = notificationRepository.findByIdAndDeletedAtIsNull(notificationId) .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); return toDetailResponse(notification); } @Transactional public AdminNoticeDetailResponse updateNotice(Long notificationId, AdminNoticeUpdateRequest request) { - Notification notification = notificationRepository.findById(notificationId) + Notification notification = notificationRepository.findByIdAndDeletedAtIsNull(notificationId) .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); notification.updateContent(request.title(), request.body()); return toDetailResponse(notification); @@ -82,9 +82,9 @@ public AdminNoticeDetailResponse updateNotice(Long notificationId, AdminNoticeUp @Transactional public void deleteNotice(Long notificationId) { - Notification notification = notificationRepository.findById(notificationId) + Notification notification = notificationRepository.findByIdAndDeletedAtIsNull(notificationId) .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); - notificationRepository.delete(notification); + notification.delete(); } private NotificationCategory normalizeCategory(NotificationCategory category) { diff --git a/src/main/java/com/swyp/picke/domain/notification/service/NotificationService.java b/src/main/java/com/swyp/picke/domain/notification/service/NotificationService.java index ea74943..5055033 100644 --- a/src/main/java/com/swyp/picke/domain/notification/service/NotificationService.java +++ b/src/main/java/com/swyp/picke/domain/notification/service/NotificationService.java @@ -153,7 +153,7 @@ public NotificationUnreadResponse hasUnread(Long userId, NotificationCategory ca } private Notification getAccessibleNotification(Long userId, Long notificationId) { - Notification notification = notificationRepository.findById(notificationId) + Notification notification = notificationRepository.findByIdAndDeletedAtIsNull(notificationId) .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); boolean isAccessible = notification.getUser() == null diff --git a/src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java b/src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java index b815356..e7259b6 100644 --- a/src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/admin/service/AdminNotificationServiceTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.swyp.picke.domain.admin.dto.notification.request.AdminNoticeUpdateRequest; @@ -51,7 +50,7 @@ private Notification newNotice(String title, String body) { @DisplayName("공지사항을 수정한다") void updateNotice_updatesExistingNotice() { Notification notification = newNotice("원본 제목", "원본 본문"); - when(notificationRepository.findById(1L)).thenReturn(Optional.of(notification)); + when(notificationRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Optional.of(notification)); AdminNoticeDetailResponse response = adminNotificationService.updateNotice( 1L, new AdminNoticeUpdateRequest("수정된 제목", "수정된 본문")); @@ -63,7 +62,7 @@ void updateNotice_updatesExistingNotice() { @Test @DisplayName("존재하지 않는 공지사항을 수정하면 예외를 던진다") void updateNotice_throws_whenNotFound() { - when(notificationRepository.findById(999L)).thenReturn(Optional.empty()); + when(notificationRepository.findByIdAndDeletedAtIsNull(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> adminNotificationService.updateNotice( 999L, new AdminNoticeUpdateRequest("제목", "본문"))) @@ -71,20 +70,20 @@ void updateNotice_throws_whenNotFound() { } @Test - @DisplayName("공지사항을 삭제한다") - void deleteNotice_removesExistingNotice() { + @DisplayName("공지사항을 삭제하면 deletedAt이 기록된다") + void deleteNotice_softDeletesExistingNotice() { Notification notification = newNotice("제목", "본문"); - when(notificationRepository.findById(1L)).thenReturn(Optional.of(notification)); + when(notificationRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Optional.of(notification)); adminNotificationService.deleteNotice(1L); - verify(notificationRepository).delete(notification); + assertThat(notification.getDeletedAt()).isNotNull(); } @Test @DisplayName("존재하지 않는 공지사항을 삭제하면 예외를 던진다") void deleteNotice_throws_whenNotFound() { - when(notificationRepository.findById(999L)).thenReturn(Optional.empty()); + when(notificationRepository.findByIdAndDeletedAtIsNull(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> adminNotificationService.deleteNotice(999L)) .isInstanceOf(CustomException.class); diff --git a/src/test/java/com/swyp/picke/domain/notification/service/NotificationServiceTest.java b/src/test/java/com/swyp/picke/domain/notification/service/NotificationServiceTest.java index 0afe16f..f413779 100644 --- a/src/test/java/com/swyp/picke/domain/notification/service/NotificationServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/notification/service/NotificationServiceTest.java @@ -164,7 +164,7 @@ void getNotifications_includes_content_broadcast_notifications() { @Test @DisplayName("존재하지 않는 알림 읽음 처리 시 예외를 던진다") void markAsRead_throws_when_not_found() { - when(notificationRepository.findById(999L)).thenReturn(Optional.empty()); + when(notificationRepository.findByIdAndDeletedAtIsNull(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> notificationService.markAsRead(1L, 999L)) .isInstanceOf(CustomException.class); @@ -184,7 +184,7 @@ void markAsRead_saves_notification_read_for_broadcast() { .referenceId(50L) .build(); - when(notificationRepository.findById(notificationId)).thenReturn(Optional.of(notification)); + when(notificationRepository.findByIdAndDeletedAtIsNull(notificationId)).thenReturn(Optional.of(notification)); when(notificationReadRepository.existsByNotificationIdAndUserId(notificationId, userId)).thenReturn(false); notificationService.markAsRead(userId, notificationId); @@ -209,7 +209,7 @@ void getNotificationDetail_returns_owned_notification() { setUserId(user, userId); setNotificationId(notification, 10L); - when(notificationRepository.findById(10L)).thenReturn(Optional.of(notification)); + when(notificationRepository.findByIdAndDeletedAtIsNull(10L)).thenReturn(Optional.of(notification)); NotificationDetailResponse response = notificationService.getNotificationDetail(userId, 10L); @@ -235,7 +235,7 @@ void getNotificationDetail_returns_broadcast_notification() { setNotificationId(notification, notificationId); - when(notificationRepository.findById(notificationId)).thenReturn(Optional.of(notification)); + when(notificationRepository.findByIdAndDeletedAtIsNull(notificationId)).thenReturn(Optional.of(notification)); when(notificationReadRepository.existsByNotificationIdAndUserId(notificationId, userId)).thenReturn(false); NotificationDetailResponse response = notificationService.getNotificationDetail(userId, notificationId); @@ -263,7 +263,7 @@ void getNotificationDetail_throws_when_notification_not_accessible() { .build(); setUserId(owner, ownerId); - when(notificationRepository.findById(30L)).thenReturn(Optional.of(notification)); + when(notificationRepository.findByIdAndDeletedAtIsNull(30L)).thenReturn(Optional.of(notification)); assertThatThrownBy(() -> notificationService.getNotificationDetail(requesterId, 30L)) .isInstanceOf(CustomException.class);