Skip to content

Commit 48e6ccd

Browse files
committed
feat: 알림 발송 서비스 및 테스트 컨트롤러 추가
1 parent 2e28569 commit 48e6ccd

4 files changed

Lines changed: 129 additions & 1 deletion

File tree

src/main/java/com/semosan/api/common/status/SuccessStatus.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ public enum SuccessStatus implements BaseStatus {
2020
/**
2121
* User
2222
*/
23-
WITHDRAW_SUCCESS(HttpStatus.OK, "USER_200_1", "회원 탈퇴가 완료되었습니다.");
23+
WITHDRAW_SUCCESS(HttpStatus.OK, "USER_200_1", "회원 탈퇴가 완료되었습니다."),
24+
25+
/**
26+
* FCM / Notification
27+
*/
28+
FCM_TOKEN_REGISTER_SUCCESS(HttpStatus.OK, "FCM_200_1", "FCM 토큰이 등록되었습니다."),
29+
FCM_TOKEN_DELETE_SUCCESS(HttpStatus.OK, "FCM_200_2", "FCM 토큰이 삭제되었습니다."),
30+
NOTIFICATION_SEND_SUCCESS(HttpStatus.OK, "NOTIF_200_1", "알림 발송 요청에 성공했습니다.");
2431

2532
private final HttpStatus httpStatus;
2633
private final String code;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.semosan.api.domain.notification.controller;
2+
3+
import com.semosan.api.common.response.ApiResponse;
4+
import com.semosan.api.common.status.SuccessStatus;
5+
import com.semosan.api.domain.notification.dto.NotificationTestRequest;
6+
import com.semosan.api.domain.notification.service.NotificationService;
7+
import jakarta.validation.Valid;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.context.annotation.Profile;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
/**
17+
* 알림 테스트용 컨트롤러. local 프로필에서만 활성화 (운영 배포 시 자동 비활성화).
18+
*/
19+
@Profile("local")
20+
@RestController
21+
@RequestMapping("/api/notifications")
22+
@RequiredArgsConstructor
23+
public class NotificationTestController {
24+
25+
private final NotificationService notificationService;
26+
27+
@PostMapping("/test")
28+
public ResponseEntity<ApiResponse<Void>> send(
29+
@Valid @RequestBody NotificationTestRequest request
30+
) {
31+
notificationService.send(request.receiverId(), request.type(), request.params());
32+
return ApiResponse.success(SuccessStatus.NOTIFICATION_SEND_SUCCESS);
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.semosan.api.domain.notification.dto;
2+
3+
import com.semosan.api.domain.notification.enums.NotificationType;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
import java.util.Map;
7+
8+
public record NotificationTestRequest(
9+
@NotNull Long receiverId,
10+
@NotNull NotificationType type,
11+
Map<String, Object> params
12+
) {
13+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.semosan.api.domain.notification.service;
2+
3+
import com.semosan.api.common.exception.GeneralException;
4+
import com.semosan.api.common.status.ErrorStatus;
5+
import com.semosan.api.domain.notification.dispatcher.NotificationDispatchCommand;
6+
import com.semosan.api.domain.notification.dispatcher.NotificationDispatcher;
7+
import com.semosan.api.domain.notification.entity.FcmToken;
8+
import com.semosan.api.domain.notification.entity.Notification;
9+
import com.semosan.api.domain.notification.enums.NotificationType;
10+
import com.semosan.api.domain.notification.repository.FcmTokenRepository;
11+
import com.semosan.api.domain.notification.repository.NotificationRepository;
12+
import com.semosan.api.domain.user.repository.UserRepository;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.stereotype.Service;
16+
import org.springframework.transaction.annotation.Transactional;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
21+
@Slf4j
22+
@Service
23+
@RequiredArgsConstructor
24+
public class NotificationService {
25+
26+
private final NotificationDispatcher dispatcher;
27+
private final NotificationRepository notificationRepository;
28+
private final FcmTokenRepository fcmTokenRepository;
29+
private final UserRepository userRepository;
30+
31+
/**
32+
* 서비스로 어디서든 한 줄로 알림 발송 가능
33+
*/
34+
@Transactional
35+
public void send(Long receiverId, NotificationType type, Map<String, Object> params) {
36+
// 1. 받는 사람 검증
37+
if (!userRepository.existsById(receiverId)) {
38+
throw new GeneralException(ErrorStatus.USER_NOT_FOUND);
39+
}
40+
41+
// 2. 파라미터 검증 (NotificationType별 필수 키)
42+
type.validate(params);
43+
44+
// 3. 템플릿 처리
45+
String title = type.formatTitle(params);
46+
String body = type.formatBody(params);
47+
48+
// 4. 이력 저장
49+
Notification notification = notificationRepository.save(
50+
Notification.create(receiverId, type, title, body, params)
51+
);
52+
53+
// 5. 토큰 조회, 없으면 정상 종료 (유저가 알림 거부/로그아웃 등 정상 케이스)
54+
List<String> tokens = fcmTokenRepository.findAllByUserId(receiverId).stream()
55+
.map(FcmToken::getToken)
56+
.toList();
57+
58+
if (tokens.isEmpty()) {
59+
log.info("발송 대상 토큰 없음 (userId={}, type={})", receiverId, type);
60+
return;
61+
}
62+
63+
// 6. 비동기 발송 위임
64+
dispatcher.dispatch(new NotificationDispatchCommand(
65+
notification.getId(),
66+
receiverId,
67+
type,
68+
title,
69+
body,
70+
params,
71+
tokens
72+
));
73+
}
74+
}

0 commit comments

Comments
 (0)