Skip to content

Commit 19b3598

Browse files
authored
Merge pull request #115 from prgrms-web-devcourse-final-project/Feature/QFEED-169-notification-imp
Feature/qfeed 169 notification imp
2 parents 0976911 + 43731f9 commit 19b3598

File tree

20 files changed

+353
-53
lines changed

20 files changed

+353
-53
lines changed

module-api/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ dependencies {
1313

1414
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
1515

16+
implementation 'org.springframework.boot:spring-boot-starter-actuator' // actuator
17+
implementation 'io.micrometer:micrometer-registry-prometheus' //prometheus
1618

1719
}
1820

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.wsws.moduleapi.notification.controller;
2+
3+
import com.wsws.moduleapi.notification.dto.NotificationTestRequest;
4+
import com.wsws.moduleapplication.notification.service.NotificationService;
5+
import com.wsws.moduleapi.notification.util.ReflectionNotificationInvoker;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
@RestController
12+
@RequestMapping("/test/notifications")
13+
@RequiredArgsConstructor
14+
@Slf4j
15+
public class NotificationTestController {
16+
17+
private final NotificationService notificationService;
18+
19+
@PostMapping("/send")
20+
public ResponseEntity<String> sendTestNotification(@RequestBody NotificationTestRequest request) {
21+
try {
22+
ReflectionNotificationInvoker.invokeSendNotification(
23+
notificationService,
24+
request.senderId(),
25+
request.recipientId(),
26+
"/test-url",
27+
request.type()
28+
);
29+
30+
return ResponseEntity.ok("테스트 알림 전송 완료됨");
31+
} catch (ClassNotFoundException e) {
32+
log.error("FcmType 클래스를 찾을 수 없습니다. type={}", request.type(), e);
33+
return ResponseEntity.internalServerError().body("FcmType 클래스 오류");
34+
} catch (IllegalArgumentException e) {
35+
log.error("존재하지 않는 FcmType: {}", request.type(), e);
36+
return ResponseEntity.badRequest().body("잘못된 FcmType");
37+
} catch (Exception e) {
38+
log.error("테스트 알림 전송 실패: type={}", request.type(), e);
39+
return ResponseEntity.internalServerError().body("테스트 실패");
40+
}
41+
}
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.wsws.moduleapi.notification.dto;
2+
3+
public record NotificationTestRequest(
4+
String senderId,
5+
String recipientId,
6+
String type
7+
) {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.wsws.moduleapi.notification.util;
2+
3+
import java.lang.reflect.Method;
4+
5+
// FcmType을 직접 접근x 테스트 시에만 접근 ( 리플렉션으로 넘김 : 유틸 클래스 사용)
6+
public class ReflectionNotificationInvoker {
7+
8+
private static final String FCM_TYPE_CLASS = "com.wsws.moduleexternalapi.fcm.util.FcmType";
9+
10+
public static void invokeSendNotification(
11+
Object service,
12+
String senderId,
13+
String recipientId,
14+
String url,
15+
String fcmTypeName
16+
) throws Exception {
17+
18+
Class<Enum> fcmTypeClass = (Class<Enum>) Class.forName(FCM_TYPE_CLASS);
19+
Enum<?> enumInstance = Enum.valueOf(fcmTypeClass, fcmTypeName);
20+
21+
Method method = service.getClass()
22+
.getMethod("sendNotification",
23+
String.class, String.class, Long.class, Long.class, Long.class, String.class, fcmTypeClass);
24+
25+
method.invoke(service,
26+
senderId,
27+
recipientId,
28+
1L,
29+
null,
30+
null,
31+
url,
32+
enumInstance
33+
);
34+
}
35+
}

module-api/src/main/resources/application.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,19 @@ springdoc:
1616
server:
1717
port: 8080
1818
address: 0.0.0.0
19+
20+
management:
21+
endpoints:
22+
web:
23+
exposure:
24+
include: "health,metrics,prometheus"
25+
metrics:
26+
enable:
27+
jvm: true
28+
endpoint:
29+
prometheus:
30+
enabled: true
31+
prometheus:
32+
metrics:
33+
export:
34+
enabled: true

module-application/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dependencies {
77

88
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
99
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
10+
implementation 'org.springframework.boot:spring-boot-starter-amqp'
1011

1112
}
1213

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.wsws.moduleapplication.notification.producer;
2+
3+
import com.wsws.moduleexternalapi.fcm.config.RabbitMQConfig;
4+
import com.wsws.moduleexternalapi.fcm.dto.FcmRequestDto;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
8+
import org.springframework.stereotype.Service;
9+
10+
@Slf4j
11+
@Service
12+
@RequiredArgsConstructor
13+
public class NotificationProducer {
14+
15+
private final RabbitTemplate rabbitTemplate;
16+
17+
public void sendNotification(FcmRequestDto dto) {
18+
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.ROUTING_KEY, dto);
19+
log.info("fcm 메시지 RabbitMQ에 저장: {}",dto);
20+
}
21+
}

module-application/src/main/java/com/wsws/moduleapplication/notification/service/NotificationService.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import com.wsws.moduleapplication.notification.dto.SaveFcmTokenRequest;
44
import com.wsws.moduleapplication.notification.dto.NotificationServiceResponse;
55
import com.wsws.moduleapplication.notification.exception.*;
6+
import com.wsws.moduleapplication.notification.producer.NotificationProducer;
67
import com.wsws.moduleapplication.usercontext.user.exception.UserNotFoundException;
78
import com.wsws.moduledomain.notification.Notification;
89
import com.wsws.moduledomain.notification.dto.NotificationDto;
910
import com.wsws.moduledomain.notification.repo.NotificationRepository;
1011
import com.wsws.moduledomain.usercontext.user.aggregate.User;
1112
import com.wsws.moduledomain.usercontext.user.repo.UserRepository;
1213
import com.wsws.moduledomain.usercontext.user.vo.UserId;
14+
import com.wsws.moduleexternalapi.fcm.dto.FcmRequestDto;
1315
import com.wsws.moduleexternalapi.fcm.service.FcmService;
1416
import com.wsws.moduleinfra.FcmRedis;
1517
import com.wsws.moduleexternalapi.fcm.util.FcmType;
@@ -34,6 +36,7 @@ public class NotificationService {
3436
private final FcmRedis fcmRedis;
3537

3638
private final AtomicLong notificationIdGenerator = new AtomicLong(1);
39+
private final NotificationProducer notificationProducer;
3740

3841
@Transactional
3942
public List<NotificationServiceResponse> getNotifications(String recipientId) {
@@ -80,15 +83,22 @@ public void sendNotification(String senderId, String recipientId, Long targetId,
8083
Long notificationId = notificationIdGenerator.getAndIncrement();
8184
String content = fcmService.makeFcmBody(fcmType, sender.getNickname().getValue());
8285

83-
// FCM 전송
84-
fcmService.fcmSend(
85-
recipient.getId().getValue(),
86-
fcmType,
87-
sender.getNickname().getValue()
88-
);
86+
if(isMqType(fcmType)) {
87+
FcmRequestDto fcmRequestDto = new FcmRequestDto(
88+
recipient.getId().getValue(),
89+
fcmType,
90+
sender.getId().getValue()
91+
);
92+
notificationProducer.sendNotification(fcmRequestDto);
93+
}
94+
95+
if (isRedisType(fcmType)) {
96+
String RedisContent = fcmService.makeFcmBody(fcmType, sender.getNickname().getValue());
97+
fcmRedis.pushNotification(recipient.getId().getValue(), RedisContent);
98+
}
8999

90100
if (shouldSkipNotificationStorage(fcmType)) {
91-
return; // CHAT는 알림 저장 x
101+
return; // CHAT 알림 저장 x
92102
}
93103

94104
// 알림 저장
@@ -147,5 +157,12 @@ private boolean shouldSkipNotificationStorage(FcmType fcmType) {
147157
return false;
148158
}
149159

160+
private boolean isMqType(FcmType type) {
161+
return type == FcmType.CHAT;
162+
}
163+
164+
private boolean isRedisType(FcmType type) {
165+
return type == FcmType.ANSWER_COMMENT || type == FcmType.ANSWER_LIKE || type == FcmType.Q_SPACE_POST_COMMENT || type == FcmType.Q_SPACE_POST_LIKE || type == FcmType.FOLLOW;
166+
}
150167

151168
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.wsws.moduleapplication.notification.service.scheduler;
2+
3+
import com.wsws.moduleexternalapi.fcm.service.FcmService;
4+
import com.wsws.moduleexternalapi.fcm.util.FcmType;
5+
import com.wsws.moduleinfra.FcmRedis;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.scheduling.annotation.Scheduled;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.util.List;
11+
import java.util.Set;
12+
13+
@Component
14+
@RequiredArgsConstructor
15+
public class NotificationBatchScheduler {
16+
17+
private final FcmService fcmService;
18+
private final FcmRedis fcmRedis;
19+
20+
@Scheduled(fixedRate = 60000)
21+
public void sendBatchNotifications() {
22+
Set<String> keys = fcmRedis.getAllNotificationKeys();
23+
24+
for (String key : keys) {
25+
String userId = key.replace("FCM_NOTIFICATION_", "");
26+
List<String> notis = fcmRedis.getNotifications(userId);
27+
28+
if (notis == null || notis.isEmpty()) continue;
29+
30+
String firstMessage = notis.get(0);
31+
int extra = notis.size() - 1;
32+
33+
String finalMessage = firstMessage;
34+
if (extra > 0) {
35+
finalMessage += " 외 " + extra + "개의 알림이 더 있습니다.";
36+
}
37+
38+
fcmService.sendFcm(userId, finalMessage, FcmType.GENERAL);
39+
fcmRedis.deleteNotifications(userId);
40+
}
41+
}
42+
}
43+

module-external-api/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dependencies {
77
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
88
implementation 'org.springframework.ai:spring-ai-redis-store-spring-boot-starter'
99
implementation 'redis.clients:jedis:5.1.0'
10+
implementation 'org.springframework.boot:spring-boot-starter-amqp'
1011

1112

1213
// Feign Client

0 commit comments

Comments
 (0)