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
17 changes: 17 additions & 0 deletions docs/api-specs/notification-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}` | 공지 수정 (제목/본문) |
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,6 +54,12 @@ public ApiResponse<AdminNoticeListResponse> getNotices(
return ApiResponse.onSuccess(adminNotificationService.getNotices(category, page, size));
}

@Operation(summary = "공지 발송 대상자 수 미리보기", description = "지금 NOTICE/EVENT 공지를 등록하면 몇 대의 디바이스에 발송되는지 미리 조회한다.")
@GetMapping("/target-count")
public ApiResponse<AdminNoticeTargetCountResponse> getTargetCount() {
return ApiResponse.onSuccess(adminNotificationService.getTargetCount());
}

@Operation(summary = "공지사항 상세 조회")
@GetMapping("/{noticeId}")
public ApiResponse<AdminNoticeDetailResponse> getNoticeDetail(@PathVariable Long noticeId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.swyp.picke.domain.admin.dto.notification.response;

public record AdminNoticeTargetCountResponse(
int targetCount
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public interface UserDeviceRepository extends JpaRepository<UserDevice, Long> {

List<UserDevice> findAllByUserIdIn(List<Long> userIds);

long countByUserIdIn(List<Long> userIds);

List<UserDevice> findAllByUserIdAndPlatform(Long userId, DevicePlatform platform);

void deleteByUserIdAndFcmToken(Long userId, String fcmToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ public void notifyAdminNotice(Long notificationId, NotificationDetailCode detail
});
}

/**
* 관리자가 공지/이벤트를 등록하기 전, 지금 등록하면 몇 명(디바이스 기준)에게 발송될지 미리 조회한다.
* {@link #notifyAdminNotice}와 동일한 대상자 산정 기준('이벤트 및 소식 알림' 설정 ON)을 사용한다.
*/
public int countAdminNoticeTargets() {
List<Long> userIds = userSettingsRepository.findUserIdsByMarketingEventEnabledTrue();
return (int) userDeviceRepository.countByUserIdIn(userIds);
}

/**
* 관리자가 특정 유저에게 알림 설정(ON/OFF) 체크 없이 즉시 테스트 푸시를 발송한다.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading