From be51bb5e36ffd589359eab492529583060148766 Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:26:52 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20ID=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=EB=94=94=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=EB=A5=BC=20=EC=84=B8=EB=8A=94=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=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 --- .../domain/notification/repository/UserDeviceRepository.java | 2 ++ 1 file changed, 2 insertions(+) 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); From 77e957021a78d8dd59c205b9de66fc9482a05e5e Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:26:56 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EB=8C=80=EC=83=81=EC=9E=90=20=EC=88=98=20=EC=82=B0?= =?UTF-8?q?=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=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/NotificationDispatchService.java | 9 +++++++++ .../service/NotificationDispatchServiceTest.java | 13 +++++++++++++ 2 files changed, 22 insertions(+) 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/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); + } } From 43004b0e6d07a71946adaca6837e5cc9d13c0e8c Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:27:01 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EB=8C=80=EC=83=81=EC=9E=90=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=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 --- .../response/AdminNoticeTargetCountResponse.java | 5 +++++ .../domain/admin/service/AdminNotificationService.java | 5 +++++ .../admin/service/AdminNotificationServiceTest.java | 10 ++++++++++ 3 files changed, 20 insertions(+) create mode 100644 src/main/java/com/swyp/picke/domain/admin/dto/notification/response/AdminNoticeTargetCountResponse.java 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/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); + } } From 2ca09f09de0afbbb04b503cecdce8f9b39d241d4 Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:27:06 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EB=8C=80=EC=83=81=EC=9E=90=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminNotificationController.java | 7 +++++++ 1 file changed, 7 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 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) { From 2ed608a6e60f7ea6fc6f3d6c335fb3cf1a9efa9a Mon Sep 17 00:00:00 2001 From: si-zero Date: Wed, 26 Aug 2026 16:27:10 +0900 Subject: [PATCH 5/5] =?UTF-8?q?docs:=20=EA=B3=B5=EC=A7=80=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EB=8C=80=EC=83=81=EC=9E=90=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=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 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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