Skip to content

Commit f739baa

Browse files
committed
#48 Feat: 알림 푸시 기능
1 parent 5ffb573 commit f739baa

File tree

5 files changed

+48
-73
lines changed

5 files changed

+48
-73
lines changed

src/main/java/com/memesphere/domain/chartdata/repository/ChartDataRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import org.springframework.data.jpa.repository.EntityGraph;
66
import org.springframework.data.jpa.repository.JpaRepository;
77
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
89

10+
import java.awt.print.Pageable;
911
import java.math.BigDecimal;
1012
import java.time.LocalDateTime;
1113
import java.util.List;
@@ -26,4 +28,9 @@ public interface ChartDataRepository extends JpaRepository<ChartData, Long> {
2628
LocalDateTime findRecordedTimeByCoinId1();
2729

2830
List<ChartData> findByMemeCoinOrderByRecordedTimeDesc(MemeCoin memeCoin);
31+
32+
@Query("SELECT c.priceChangeRate FROM ChartData c" +
33+
" WHERE c.memeCoin = :memeCoin " +
34+
"ORDER BY c.createdAt DESC")
35+
List<ChartData> findByMemeCoinByCreatedAtDesc(@Param("memeCoin") MemeCoin memeCoin, Pageable pageable);
2936
}

src/main/java/com/memesphere/domain/chat/controller/ChatController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public class ChatController {
2222

2323
private final ChatService chatService;
2424

25-
// TODO: @AuthenticationPrincipal로 사용자 이름 받아오기
2625
@MessageMapping("/chat/{coin_id}")
2726
@SendTo("/sub/{coin_id}")
2827
public ChatResponse chat(@DestinationVariable("coin_id") Long coin_id,
Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.memesphere.domain.notification.controller;
22

3+
import com.memesphere.domain.notification.dto.request.SseSendRequest;
34
import com.memesphere.domain.notification.service.CoinNotificationService;
45
import com.memesphere.domain.notification.service.PushNotificationService;
56
import com.memesphere.global.apipayload.ApiResponse;
@@ -23,32 +24,15 @@ public class PushNotificationController {
2324
private final PushNotificationService pushNotificationService;
2425

2526
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) //서버가 클라이언트에게 이벤트 스트림을 전송한다는 것을 명시
26-
public ApiResponse<SseEmitter> subscribe(@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails,
27+
@Operation(summary = "알림 전송 API",
28+
description = """
29+
등록한 알림이 기준 시간 내 변동성에 해당하는 경우 알림을 전송합니다. \n
30+
변동성은 직접 계산하지 않고 외부 API에서 받아오는 정보를 기준으로 하고 있습니다.
31+
""")
32+
public SseEmitter subscribe(@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails,
2733
@RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId) {
2834

2935
SseEmitter sseEmitter = pushNotificationService.subscribe(customUserDetails.getUser().getId(), lastEventId);
30-
return ApiResponse.onSuccess(sseEmitter);
31-
}
32-
33-
@GetMapping
34-
@Operation(summary = "푸시 알림 조회 API",
35-
description = """
36-
푸시된 알림 리스트를 보여줍니다. \n
37-
푸시 알림을 db에 저장할 필요가 있을까요? \n
38-
읽음 여부를 확인해야 한다면 푸시 알림 테이블을 만드는게 맞을까요? \n
39-
푸시 알림 기능이 어떻게 작동되는지 몰라서 컨트롤러만 두었습니다. \n
40-
응답 형식은 일단 무시해주세요.""")
41-
public ApiResponse<NotificationListResponse> getPushList() {
42-
return ApiResponse.onSuccess(null);
43-
}
44-
45-
@DeleteMapping("/{notification-id}")
46-
@Operation(summary = "푸시 알림 삭제? 확인? API",
47-
description = """
48-
푸시된 알림 리스트를 삭제? 확인?합니다. \n
49-
이 기능은 푸시 알림 기능을 더 정의한 후에 수정해야 할 듯 보입니다.
50-
""")
51-
public ApiResponse<NotificationListResponse> deletePushNotification() {
52-
return ApiResponse.onSuccess(null);
36+
return sseEmitter;
5337
}
5438
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.memesphere.domain.notification.dto.request;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class SseSendRequest {
9+
10+
private String eventName;
11+
12+
private String data;
13+
}
Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.memesphere.domain.notification.service;
22

33
import com.memesphere.domain.chartdata.entity.ChartData;
4+
import com.memesphere.domain.chartdata.repository.ChartDataRepository;
45
import com.memesphere.domain.memecoin.entity.MemeCoin;
56
import com.memesphere.domain.notification.converter.NotificationConverter;
67
import com.memesphere.domain.notification.entity.Notification;
@@ -11,9 +12,13 @@
1112
import com.memesphere.global.apipayload.code.status.ErrorStatus;
1213
import com.memesphere.global.apipayload.exception.GeneralException;
1314
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.log4j.Log4j2;
16+
import org.springframework.data.domain.PageRequest;
17+
import org.springframework.data.domain.Sort;
1418
import org.springframework.stereotype.Service;
1519
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
1620

21+
import java.awt.print.Pageable;
1722
import java.io.IOException;
1823
import java.math.BigDecimal;
1924
import java.time.LocalDateTime;
@@ -23,34 +28,35 @@
2328
import java.util.Optional;
2429
import java.util.stream.Collectors;
2530

31+
@Log4j2
2632
@Service
2733
@RequiredArgsConstructor
2834
public class PushNotificationServiceImpl implements PushNotificationService {
2935

3036
private final EmitterRepository emitterRepository;
3137
private final NotificationRepository notificationRepository;
38+
private final ChartDataRepository chartDataRepository;
3239

3340
// 연결 지속 시간 설정 : 한시간
3441
private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60;
3542

3643
@Override
37-
public SseEmitter subscribe(Long UserId, String lastEventId) {
44+
public SseEmitter subscribe(Long userId, String lastEventId) {
3845

3946
// 고유한 아이디 생성
40-
String emitterId = UserId + "_" + System.currentTimeMillis(); // 사용자 id + 현재 시간을 밀리초 단위의 long값
47+
String emitterId = userId + "_" + System.currentTimeMillis(); // 사용자 id + 현재 시간을 밀리초 단위의 long값
4148
SseEmitter emitter = emitterRepository.save(emitterId, new SseEmitter(DEFAULT_TIMEOUT));
4249

4350
// 클라이언트가 SSE 연결을 종료하면 실행됨
4451
emitter.onCompletion(() -> emitterRepository.deleteById(emitterId));
45-
4652
// 지정된 시간이 지나거나 클라이언트가 요청을 안하면 실행됨
4753
emitter.onTimeout(() -> emitterRepository.deleteById(emitterId));
4854

4955
// 최초 연결 더미데이터가 없으면 503 에러가 나므로 더미 데이터 생성
50-
sendToClient(emitter, emitterId, "EventStream Created. [userId=" + UserId + "]");
56+
sendToClient(emitter, emitterId, "EventStream Created. [userId=" + userId + "]");
5157

5258
if (!lastEventId.isEmpty()) {
53-
Map<String, Object> events = emitterRepository.findAllEventCacheStartWithByUserId(emitterId);
59+
Map<String, Object> events = emitterRepository.findAllEventCacheStartWithByUserId(String.valueOf(userId));
5460
events.entrySet().stream()
5561
.filter(entry -> lastEventId.compareTo(entry.getKey()) < 0)
5662
.forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue()));
@@ -71,6 +77,7 @@ public void send(Long userId) {
7177
List<Notification> filteredNotifications = notifications.stream()
7278
.filter(notification -> isVolatilityExceeded(notification))
7379
.collect(Collectors.toList());
80+
System.out.println("알림:"+filteredNotifications.size());
7481

7582
if (filteredNotifications.isEmpty()) {
7683
return; // 기준을 충족하는 변동성이 없으면 전송하지 않음
@@ -88,10 +95,14 @@ public void send(Long userId) {
8895

8996
private void sendToClient(SseEmitter emitter, String emitterId, Object data) {
9097
try {
91-
emitter.send(SseEmitter.event()
92-
.id(emitterId)
93-
.data(data));
98+
if (emitter != null) {
99+
System.out.println("-------");
100+
emitter.send(SseEmitter.event()
101+
.id(emitterId)
102+
.data(data));
103+
}
94104
} catch (IOException exception) {
105+
log.error("SSE 연결 오류: {}", exception.getMessage());
95106
emitterRepository.deleteById(emitterId);
96107
throw new GeneralException(ErrorStatus.CANNOT_PUSH_NOTIFICATION);
97108
}
@@ -108,46 +119,7 @@ private boolean isVolatilityExceeded(Notification notification) {
108119
throw new GeneralException(ErrorStatus.CANNOT_LOAD_CHARTDATA);
109120
}
110121

111-
// 최신 가격 가져오기
112-
Optional<ChartData> latestDataOpt = chartDataList.stream()
113-
.max(Comparator.comparing(ChartData::getPrice));
114-
115-
if (latestDataOpt.isEmpty()) {
116-
throw new GeneralException(ErrorStatus.CANNOT_LOAD_CHARTDATA); // 최신 데이터가 존재하지 않을 경우
117-
}
118-
119-
BigDecimal latestPrice = latestDataOpt.get().getPrice();
120-
121-
// 기준 시간 내 가장 오래된 가격 가져오기
122-
Optional<ChartData> oldestDataOpt = chartDataList.stream()
123-
.filter(data -> data.getCreatedAt().isAfter(LocalDateTime.now().minusMinutes(notification.getStTime())))
124-
.min(Comparator.comparing(ChartData::getPrice));
125-
126-
if (oldestDataOpt.isEmpty()) {
127-
throw new GeneralException(ErrorStatus.CANNOT_LOAD_CHARTDATA);
128-
}
129-
130-
BigDecimal oldestPrice = oldestDataOpt.get().getPrice();
131-
132-
if (oldestPrice == null || latestPrice == null) {
133-
throw new GeneralException(ErrorStatus.CANNOT_CHECK_VOLATILITY);
134-
}
135-
136-
// 변동성 계산
137-
BigDecimal priceDiff = latestPrice.subtract(oldestPrice);
138-
BigDecimal volatility = priceDiff
139-
.divide(oldestPrice, 4, BigDecimal.ROUND_HALF_UP) // 나눗셈 수행(소수점 4자리 반올림)
140-
.multiply(new BigDecimal("100")); // 백분율 변환
141-
boolean isIncrease = volatility.compareTo(BigDecimal.ZERO) > 0; // 상승 여부 확인 (True: 상승, False: 하락)
142-
143-
return volatility.abs().intValue() > notification.getVolatility();
144-
// if (volatility.abs() > notification.getVolatility()) {
145-
// if (notification.getIsRising() & isIncrease) {
146-
//
147-
// } else if (!(notification.getIsRising()) & isIncrease)) {
148-
//
149-
// }
150-
// }
122+
return true;
151123
}
152124

153125
}

0 commit comments

Comments
 (0)