diff --git a/docs/api-specs/notification-api.md b/docs/api-specs/notification-api.md index d9a0b1d..6b6dac0 100644 --- a/docs/api-specs/notification-api.md +++ b/docs/api-specs/notification-api.md @@ -345,6 +345,7 @@ | Method | Path | 설명 | |---|---|---| | `POST` | `/api/v1/admin/notices` | 공지사항/이벤트 작성 (즉시 전체 발송) | +| `GET` | `/api/v1/admin/notices/target-count` | 지금 등록하면 몇 대의 디바이스에 발송되는지 미리 조회 | | `GET` | `/api/v1/admin/notices` | 공지 목록 조회 (`category`, `page`, `size` 쿼리) | | `GET` | `/api/v1/admin/notices/{noticeId}` | 공지 상세 조회 | | `PUT` | `/api/v1/admin/notices/{noticeId}` | 공지 수정 (제목/본문) | @@ -370,6 +371,22 @@ 응답은 `AdminNoticeDetailResponse` (`notificationId`, `category`, `detailCode`, `title`, `body`, `referenceId`, `createdAt`). +**`GET /api/v1/admin/notices/target-count`** + +`NOTICE`/`EVENT` 공지를 지금 등록하면 몇 대의 디바이스에 발송되는지 미리 조회합니다. `notifyAdminNotice`가 사용하는 것과 동일하게 '이벤트 및 소식 알림' 설정이 ON인 유저의 등록된 디바이스 수 기준입니다. `CONTENT` 카테고리 공지는 애초에 이 대상자 산정을 타지 않으므로 참고용으로만 사용하세요. + +성공 응답 `200 OK`: + +```json +{ + "statusCode": 200, + "data": { + "targetCount": 1200 + }, + "error": null +} +``` + **`PUT /api/v1/admin/notices/{noticeId}` 요청 바디** ```json 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 316acbe..be0f213 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 @@ -6,6 +6,7 @@ import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeDeliveryResultResponse; 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.AdminNoticeTargetCountResponse; import com.swyp.picke.domain.admin.service.AdminNotificationService; import com.swyp.picke.domain.notification.enums.NotificationCategory; import com.swyp.picke.domain.notification.service.NotificationDispatchService; @@ -53,6 +54,12 @@ public ApiResponse getNotices( return ApiResponse.onSuccess(adminNotificationService.getNotices(category, page, size)); } + @Operation(summary = "공지 발송 대상자 수 미리보기", description = "지금 NOTICE/EVENT 공지를 등록하면 몇 대의 디바이스에 발송되는지 미리 조회한다.") + @GetMapping("/target-count") + public ApiResponse getTargetCount() { + return ApiResponse.onSuccess(adminNotificationService.getTargetCount()); + } + @Operation(summary = "공지사항 상세 조회") @GetMapping("/{noticeId}") public ApiResponse getNoticeDetail(@PathVariable Long noticeId) { diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/notification/response/AdminNoticeTargetCountResponse.java b/src/main/java/com/swyp/picke/domain/admin/dto/notification/response/AdminNoticeTargetCountResponse.java new file mode 100644 index 0000000..b0f3edb --- /dev/null +++ b/src/main/java/com/swyp/picke/domain/admin/dto/notification/response/AdminNoticeTargetCountResponse.java @@ -0,0 +1,5 @@ +package com.swyp.picke.domain.admin.dto.notification.response; + +public record AdminNoticeTargetCountResponse( + int targetCount +) {} 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 ec436a2..55445fc 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 @@ -6,6 +6,7 @@ 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; +import com.swyp.picke.domain.admin.dto.notification.response.AdminNoticeTargetCountResponse; import com.swyp.picke.domain.notification.entity.Notification; import com.swyp.picke.domain.notification.entity.NotificationDeliveryResult; import com.swyp.picke.domain.notification.enums.NotificationCategory; @@ -107,6 +108,10 @@ public AdminNoticeDeliveryResultResponse getDeliveryResult(Long notificationId) ); } + public AdminNoticeTargetCountResponse getTargetCount() { + return new AdminNoticeTargetCountResponse(notificationDispatchService.countAdminNoticeTargets()); + } + private NotificationCategory normalizeCategory(NotificationCategory category) { if (category == null || category == NotificationCategory.ALL) { return null; diff --git a/src/main/java/com/swyp/picke/domain/notification/repository/UserDeviceRepository.java b/src/main/java/com/swyp/picke/domain/notification/repository/UserDeviceRepository.java index e6f3869..f377bc4 100644 --- a/src/main/java/com/swyp/picke/domain/notification/repository/UserDeviceRepository.java +++ b/src/main/java/com/swyp/picke/domain/notification/repository/UserDeviceRepository.java @@ -15,6 +15,8 @@ public interface UserDeviceRepository extends JpaRepository { List findAllByUserIdIn(List userIds); + long countByUserIdIn(List userIds); + List findAllByUserIdAndPlatform(Long userId, DevicePlatform platform); void deleteByUserIdAndFcmToken(Long userId, String fcmToken); diff --git a/src/main/java/com/swyp/picke/domain/notification/service/NotificationDispatchService.java b/src/main/java/com/swyp/picke/domain/notification/service/NotificationDispatchService.java index 02c34aa..eab6133 100644 --- a/src/main/java/com/swyp/picke/domain/notification/service/NotificationDispatchService.java +++ b/src/main/java/com/swyp/picke/domain/notification/service/NotificationDispatchService.java @@ -116,6 +116,15 @@ public void notifyAdminNotice(Long notificationId, NotificationDetailCode detail }); } + /** + * 관리자가 공지/이벤트를 등록하기 전, 지금 등록하면 몇 명(디바이스 기준)에게 발송될지 미리 조회한다. + * {@link #notifyAdminNotice}와 동일한 대상자 산정 기준('이벤트 및 소식 알림' 설정 ON)을 사용한다. + */ + public int countAdminNoticeTargets() { + List userIds = userSettingsRepository.findUserIdsByMarketingEventEnabledTrue(); + return (int) userDeviceRepository.countByUserIdIn(userIds); + } + /** * 관리자가 특정 유저에게 알림 설정(ON/OFF) 체크 없이 즉시 테스트 푸시를 발송한다. */ 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 19c2d8e..621b5da 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 @@ -134,4 +134,14 @@ void getDeliveryResult_throws_whenResultNotFound() { assertThatThrownBy(() -> adminNotificationService.getDeliveryResult(1L)) .isInstanceOf(CustomException.class); } + + @Test + @DisplayName("공지 발송 대상자 수를 조회한다") + void getTargetCount_returnsCount() { + when(notificationDispatchService.countAdminNoticeTargets()).thenReturn(1200); + + var response = adminNotificationService.getTargetCount(); + + assertThat(response.targetCount()).isEqualTo(1200); + } } diff --git a/src/test/java/com/swyp/picke/domain/notification/service/NotificationDispatchServiceTest.java b/src/test/java/com/swyp/picke/domain/notification/service/NotificationDispatchServiceTest.java index 15f0812..04d7532 100644 --- a/src/test/java/com/swyp/picke/domain/notification/service/NotificationDispatchServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/notification/service/NotificationDispatchServiceTest.java @@ -82,4 +82,17 @@ void notifyAdminNotice_recordsDeliveryResult() { verify(notificationDeliveryResultRepository).updateResult(10L, 1, 1); } + + @Test + @DisplayName("공지 발송 대상자 수를 마케팅/이벤트 알림 설정 ON인 유저의 디바이스 수 기준으로 조회한다") + void countAdminNoticeTargets_returnsDeviceCount() { + NotificationDispatchService notificationDispatchService = newService(); + + when(userSettingsRepository.findUserIdsByMarketingEventEnabledTrue()).thenReturn(List.of(1L, 2L, 3L)); + when(userDeviceRepository.countByUserIdIn(List.of(1L, 2L, 3L))).thenReturn(5L); + + int targetCount = notificationDispatchService.countAdminNoticeTargets(); + + assertThat(targetCount).isEqualTo(5); + } }