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
11 changes: 11 additions & 0 deletions docs/api-specs/notification-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@
| `GET` | `/api/v1/admin/notification-schedules/{scheduleId}` | 예약 알림 상세 조회 |
| `PUT` | `/api/v1/admin/notification-schedules/{scheduleId}` | 예약 알림 수정 (제목/부제목/발송시간/on-off 전체 교체) |
| `PATCH` | `/api/v1/admin/notification-schedules/{scheduleId}/toggle` | 예약 알림 On/Off만 전환 |
| `POST` | `/api/v1/admin/notification-schedules/{scheduleId}/test` | 저장된 제목/부제목으로 특정 유저에게 테스트 발송 |
| `DELETE` | `/api/v1/admin/notification-schedules/{scheduleId}` | 예약 알림 삭제 |

**`POST` / `PUT` 요청 바디**
Expand Down Expand Up @@ -474,6 +475,16 @@
}
```

**`POST .../test` 요청 바디**

```json
{
"userId": 123
}
```

저장되어 있는 예약 알림의 `title`/`subtitle`을 그대로 사용해, 지정한 유저의 등록된 디바이스로 알림 설정(ON/OFF) 무관하게 즉시 테스트 푸시를 발송합니다. 등록/수정 폼에서 저장 전 미리보기 용도로 쓰려면, 먼저 원하는 문구로 `POST`/`PUT`을 호출해 저장한 뒤 이 API로 확인하는 흐름을 권장합니다.

**응답 (`AdminNotificationScheduleResponse`)**

```json
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swyp.picke.domain.admin.controller;

import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleTestRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleToggleRequest;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNotificationScheduleListResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNotificationScheduleResponse;
Expand Down Expand Up @@ -68,6 +69,16 @@ public ApiResponse<AdminNotificationScheduleResponse> toggle(
return ApiResponse.onSuccess(adminNotificationScheduleService.toggle(scheduleId, request));
}

@Operation(summary = "예약 알림 테스트 발송", description = "저장된 예약 알림의 제목/부제목을 특정 유저에게 알림 설정(ON/OFF) 무관하게 즉시 테스트 발송한다.")
@PostMapping("/{scheduleId}/test")
public ApiResponse<Void> sendTest(
@PathVariable Long scheduleId,
@RequestBody @Valid AdminNotificationScheduleTestRequest request
) {
adminNotificationScheduleService.sendTest(scheduleId, request);
return ApiResponse.onSuccess(null);
}

@Operation(summary = "예약 알림 삭제")
@DeleteMapping("/{scheduleId}")
public ApiResponse<Void> delete(@PathVariable Long scheduleId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.swyp.picke.domain.admin.dto.notification.request;

import jakarta.validation.constraints.NotNull;

public record AdminNotificationScheduleTestRequest(
@NotNull Long userId
) {}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.swyp.picke.domain.admin.service;

import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleTestRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleToggleRequest;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNotificationScheduleListResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNotificationScheduleResponse;
import com.swyp.picke.domain.notification.entity.NotificationSchedule;
import com.swyp.picke.domain.notification.repository.NotificationScheduleRepository;
import com.swyp.picke.domain.notification.service.NotificationDispatchService;
import com.swyp.picke.global.common.exception.CustomException;
import com.swyp.picke.global.common.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,6 +20,7 @@
public class AdminNotificationScheduleService {

private final NotificationScheduleRepository notificationScheduleRepository;
private final NotificationDispatchService notificationDispatchService;

@Transactional
public AdminNotificationScheduleResponse create(AdminNotificationScheduleRequest request) {
Expand Down Expand Up @@ -57,6 +60,11 @@ public AdminNotificationScheduleResponse toggle(Long scheduleId, AdminNotificati
return toResponse(schedule);
}

public void sendTest(Long scheduleId, AdminNotificationScheduleTestRequest request) {
NotificationSchedule schedule = getExistingSchedule(scheduleId);
notificationDispatchService.sendTestPush(request.userId(), schedule.getTitle(), schedule.getSubtitle());
}

@Transactional
public void delete(Long scheduleId) {
NotificationSchedule schedule = getExistingSchedule(scheduleId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
import static org.mockito.Mockito.when;

import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleTestRequest;
import com.swyp.picke.domain.admin.dto.notification.request.AdminNotificationScheduleToggleRequest;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNotificationScheduleListResponse;
import com.swyp.picke.domain.admin.dto.notification.response.AdminNotificationScheduleResponse;
import com.swyp.picke.domain.notification.entity.NotificationSchedule;
import com.swyp.picke.domain.notification.repository.NotificationScheduleRepository;
import com.swyp.picke.domain.notification.service.NotificationDispatchService;
import com.swyp.picke.global.common.exception.CustomException;
import java.time.LocalTime;
import java.util.List;
Expand All @@ -29,6 +31,9 @@ class AdminNotificationScheduleServiceTest {
@Mock
private NotificationScheduleRepository notificationScheduleRepository;

@Mock
private NotificationDispatchService notificationDispatchService;

@InjectMocks
private AdminNotificationScheduleService adminNotificationScheduleService;

Expand Down Expand Up @@ -133,6 +138,32 @@ void toggle_throws_whenNotFound() {
.isInstanceOf(CustomException.class);
}

@Test
@DisplayName("예약 알림 저장 내용으로 특정 유저에게 테스트 발송한다")
void sendTest_dispatchesSavedScheduleContent() {
NotificationSchedule schedule = NotificationSchedule.builder()
.title("오늘의 질문")
.subtitle("지금 확인해보세요")
.sendTime(LocalTime.of(19, 0))
.enabled(true)
.build();
when(notificationScheduleRepository.findById(1L)).thenReturn(Optional.of(schedule));

adminNotificationScheduleService.sendTest(1L, new AdminNotificationScheduleTestRequest(123L));

verify(notificationDispatchService).sendTestPush(123L, "오늘의 질문", "지금 확인해보세요");
}

@Test
@DisplayName("존재하지 않는 예약 알림을 테스트 발송하면 예외를 던진다")
void sendTest_throws_whenNotFound() {
when(notificationScheduleRepository.findById(999L)).thenReturn(Optional.empty());

assertThatThrownBy(() -> adminNotificationScheduleService.sendTest(
999L, new AdminNotificationScheduleTestRequest(123L)))
.isInstanceOf(CustomException.class);
}

@Test
@DisplayName("예약 알림을 삭제한다")
void delete_removesExistingSchedule() {
Expand Down
Loading