Skip to content

Commit e231d05

Browse files
authored
Merge pull request #185 from prgrms-web-devcourse-final-project/feature/apply-alarm-method-2(WR9-111)
Feature/apply alarm method 2(wr9 111)
2 parents 2ded27b + 500342e commit e231d05

File tree

11 files changed

+162
-27
lines changed

11 files changed

+162
-27
lines changed

src/main/java/io/crops/warmletter/domain/letter/repository/LetterRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.crops.warmletter.domain.letter.entity.Letter;
66
import io.crops.warmletter.domain.letter.enums.Category;
77
import io.crops.warmletter.domain.letter.enums.Status;
8+
import io.crops.warmletter.domain.timeline.dto.response.LetterAlarmResponse;
89
import io.lettuce.core.dynamic.annotation.Param;
910
import org.springframework.data.domain.Page;
1011
import org.springframework.data.domain.Pageable;
@@ -14,6 +15,7 @@
1415

1516
import java.time.LocalDateTime;
1617
import java.util.List;
18+
import java.util.Map;
1719
import java.util.Optional;
1820

1921

@@ -66,4 +68,12 @@ List<LetterDraftResponse> findDraftLettersWithMatching(@Param("writerId") Long w
6668
Optional<Letter> findByIdAndWriterIdAndStatusIsSAVED(Long id, Long writerId);
6769

6870
List<Letter> findByStatusAndDeliveryCompletedAtLessThanEqual(Status status, LocalDateTime now);
71+
72+
@Query("SELECT new io.crops.warmletter.domain.timeline.dto.response.LetterAlarmResponse(" +
73+
"l.writerId, m.zipCode) " +
74+
"FROM Letter l " +
75+
"JOIN Member m ON l.writerId = m.id " +
76+
"WHERE l.status = 'IN_DELIVERY' " +
77+
"AND l.deliveryCompletedAt <= :now")
78+
List<LetterAlarmResponse> findZipCodeByLettersToComplete(LocalDateTime now);
6979
}

src/main/java/io/crops/warmletter/domain/letter/service/LetterService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ public LetterResponse createLetter(CreateLetterRequest request) {
9999

100100
String zipCode = authFacade.getZipCode();
101101

102+
// 알림 전송
102103
if(request.getReceiverId() != null){
103-
notificationFacade.sendNotification(zipCode,request.getReceiverId(), AlarmType.LETTER,savedLetter.getId().toString());
104+
notificationFacade.sendNotification(zipCode,request.getReceiverId(), AlarmType.SENDING,null);
104105
}
105106

106107
return LetterResponse.fromEntity(savedLetter, zipCode);

src/main/java/io/crops/warmletter/domain/report/service/ReportService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public UpdateReportResponse updateReport(Long reportId, UpdateReportRequest requ
8080
memberRepository.save(reportedMember);
8181
}
8282
resolvePendingReports(report);
83-
// targetMemberId로 알림 전송 TODO : 배포 후 테스트 예정
83+
// targetMemberId로 알림 전송
8484
notificationFacade.sendNotification(null, targetMemberId, AlarmType.REPORT, report.getAdminMemo()+"§"+reportedMember.getWarningCount());
8585
}
8686
return new UpdateReportResponse(report,reportedMember);

src/main/java/io/crops/warmletter/domain/share/service/ShareProposalService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public ShareProposalResponse requestShareProposal(ShareProposalRequest request)
4545
if (response == null) {
4646
throw new ShareProposalNotFoundException();
4747
}
48-
// 알림 전송 TODO : 배포 후 테스트 예정
48+
// 알림 전송
4949
notificationFacade.sendNotification(response.getZipCode(), request.getRecipientId(), AlarmType.SHARE, response.getShareProposalId().toString());
5050
return response;
5151
}
@@ -71,7 +71,7 @@ public ShareProposalStatusResponse approveShareProposal(Long shareProposalId) {
7171
.isActive(true)
7272
.build();
7373
sharePost = sharePostRepository.save(sharePost);
74-
// 알림 전송(양쪽다) / 인가 값이 없어서 일단 우편번호는 임시값으로 대체 TODO : 배포 후 테스트 예정
74+
// 알림 전송(양쪽다) / 인가 값이 없어서 일단 우편번호는 임시값으로 대체
7575
notificationFacade.sendNotification("승인요청자", shareProposal.getRequesterId(), AlarmType.POSTED, sharePost.getId().toString());
7676
notificationFacade.sendNotification("승인수락자", shareProposal.getRecipientId(), AlarmType.POSTED, sharePost.getId().toString());
7777
return ShareProposalStatusResponse.builder()

src/main/java/io/crops/warmletter/domain/timeline/controller/NotificationController.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
package io.crops.warmletter.domain.timeline.controller;
22

3-
import io.crops.warmletter.domain.timeline.dto.response.ReadNotificationResponse;
43
import io.crops.warmletter.domain.timeline.service.NotificationService;
5-
import io.crops.warmletter.global.response.BaseResponse;
64
import io.swagger.v3.oas.annotations.Operation;
75
import io.swagger.v3.oas.annotations.tags.Tag;
86
import lombok.RequiredArgsConstructor;
97
import org.springframework.http.MediaType;
10-
import org.springframework.http.ResponseEntity;
118
import org.springframework.web.bind.annotation.*;
129
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
1310

14-
import java.util.List;
15-
1611
@RestController
1712
@RequiredArgsConstructor
1813
@RequestMapping("/api/notifications")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.crops.warmletter.domain.timeline.dto.response;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class LetterAlarmResponse {
9+
private Long writerId;
10+
private String zipCode;
11+
}

src/main/java/io/crops/warmletter/domain/timeline/enums/AlarmType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
public enum AlarmType {
44
// 편지수신, 신고처리, 공유요청, 게시판 게시됨
5-
LETTER, REPORT, SHARE, POSTED
5+
SENDING, LETTER, REPORT, SHARE, POSTED
66
}

src/main/java/io/crops/warmletter/domain/timeline/service/NotificationService.java

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,22 @@
22

33
import io.crops.warmletter.domain.auth.facade.AuthFacade;
44
import io.crops.warmletter.domain.timeline.dto.response.NotificationResponse;
5-
import io.crops.warmletter.domain.timeline.dto.response.ReadNotificationResponse;
65
import io.crops.warmletter.domain.timeline.entity.Timeline;
76
import io.crops.warmletter.domain.timeline.enums.AlarmType;
8-
import io.crops.warmletter.domain.timeline.exception.NotificationNotFoundException;
97
import io.crops.warmletter.domain.timeline.repository.TimelineRepository;
108
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
1110
import org.springframework.http.MediaType;
1211
import org.springframework.scheduling.annotation.Scheduled;
1312
import org.springframework.stereotype.Service;
1413
import org.springframework.transaction.annotation.Transactional;
1514
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
1615

1716
import java.io.IOException;
18-
import java.util.ArrayList;
19-
import java.util.List;
2017
import java.util.Map;
2118
import java.util.concurrent.ConcurrentHashMap;
2219

20+
@Slf4j
2321
@Service
2422
@RequiredArgsConstructor
2523
public class NotificationService {
@@ -33,8 +31,8 @@ public SseEmitter subscribeNotification(){
3331

3432
emitters.put(memberId, emitter);
3533

36-
emitter.onCompletion(() -> emitters.remove(memberId)); // 연결 종료 시 제거
37-
emitter.onTimeout(() -> emitters.remove(memberId)); // 타임아웃 시 제거
34+
emitter.onCompletion(() -> handleCompletion(memberId));
35+
emitter.onTimeout(() -> handleTimeout(memberId, emitter));
3836

3937
NotificationResponse notificationResponse = NotificationResponse.builder()
4038
.title("사용자 " + memberId + " EventStream 생성")
@@ -45,6 +43,17 @@ public SseEmitter subscribeNotification(){
4543
return emitter;
4644
}
4745

46+
protected void handleCompletion(Long memberId) {
47+
emitters.remove(memberId);
48+
log.info("SSE 연결 종료");
49+
}
50+
51+
protected void handleTimeout(Long memberId, SseEmitter emitter) {
52+
emitters.remove(memberId);
53+
log.info("SSE 연결 타임아웃 발생");
54+
emitter.complete();
55+
}
56+
4857
// 편지 수신, 신고 조치, 공유 요청, 공유 게시글 등록 시 호출 필요
4958
@Transactional
5059
public void createNotification(String senderZipCode, Long receiverId, AlarmType alarmType, String data){
@@ -55,9 +64,12 @@ public void createNotification(String senderZipCode, Long receiverId, AlarmType
5564
.alarmType(alarmType);
5665

5766
switch(alarmType) {
58-
case LETTER:
67+
case SENDING:
5968
builder.title(senderZipCode+"님이 편지를 보냈습니다.");
6069
break;
70+
case LETTER:
71+
builder.title(senderZipCode+"님의 편지가 도착했습니다.");
72+
break;
6173
case REPORT:
6274
builder.title("따숨님, 최근 활동에 대해 경고를 받으셨어요.");
6375
break;
@@ -82,16 +94,17 @@ public void createNotification(String senderZipCode, Long receiverId, AlarmType
8294
sendEventToClient(receiverId,notificationResponse);
8395
}
8496

85-
private void sendEventToClient(Long receiverId, NotificationResponse notificationResponse){
97+
protected void sendEventToClient(Long receiverId, NotificationResponse notificationResponse){
8698
SseEmitter emitter = emitters.get(receiverId);
8799
if (emitter != null) {
88100
try {
89101
emitter.send(SseEmitter.event()
90102
.data(notificationResponse, MediaType.APPLICATION_JSON));
103+
log.info("사용자 ID : {}으로 알림 전송 성공", receiverId);
91104
} catch (IOException e) {
92-
emitter.complete();
93105
emitters.remove(receiverId);
94-
106+
log.warn("사용자 ID : {}으로 알림 전송 실패",receiverId);
107+
emitter.complete();
95108
}
96109
}
97110
}
@@ -107,8 +120,9 @@ public void sendHeartbeat() {
107120
.name("heartbeat")
108121
.data("ping"));
109122
} catch (IOException e) {
110-
emitter.complete();
111123
emitters.remove(memberId);
124+
log.warn("사용자 ID : {} 대상 Heartbeat 전송 실패",memberId);
125+
emitter.complete();
112126
}
113127
}
114128
}

src/main/java/io/crops/warmletter/global/schedule/DeliverySchedule.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import io.crops.warmletter.domain.letter.entity.Letter;
44
import io.crops.warmletter.domain.letter.enums.Status;
55
import io.crops.warmletter.domain.letter.repository.LetterRepository;
6+
import io.crops.warmletter.domain.member.repository.MemberRepository;
7+
import io.crops.warmletter.domain.timeline.dto.response.LetterAlarmResponse;
8+
import io.crops.warmletter.domain.timeline.enums.AlarmType;
9+
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
610
import jakarta.transaction.Transactional;
711
import lombok.RequiredArgsConstructor;
812
import lombok.extern.slf4j.Slf4j;
@@ -12,13 +16,16 @@
1216
import java.time.LocalDateTime;
1317
import java.time.format.DateTimeFormatter;
1418
import java.util.List;
19+
import java.util.Map;
20+
import java.util.stream.Collectors;
1521

1622
@Slf4j
1723
@Configuration
1824
@RequiredArgsConstructor
1925
public class DeliverySchedule {
2026

2127
private final LetterRepository letterRepository;
28+
private final NotificationFacade notificationFacade;
2229

2330
@Transactional
2431
@Scheduled(cron = "0 */1 * * * *", zone = "Asia/Seoul")
@@ -31,6 +38,10 @@ public void processDeliveryCompletion() {
3138
// 배송 완료 조건을 만족하는 편지 목록 조회 (배송 중이면서 배송 완료 시간이 현재보다 이전인 편지)
3239
List<Letter> lettersToComplete = letterRepository.findByStatusAndDeliveryCompletedAtLessThanEqual(
3340
Status.IN_DELIVERY, now);
41+
// lettersToComplete 조건을 만족하는 편지를 보낸 사람의 zipCode 조회
42+
List<LetterAlarmResponse> zipCodeData = letterRepository.findZipCodeByLettersToComplete(now);
43+
Map<Long, String> senderZipCodes = zipCodeData.stream()
44+
.collect(Collectors.toMap(LetterAlarmResponse::getWriterId, LetterAlarmResponse::getZipCode));
3445

3546
if (!lettersToComplete.isEmpty()) {
3647
log.info("배송 완료 처리할 편지 수: {}", lettersToComplete.size());
@@ -39,6 +50,12 @@ public void processDeliveryCompletion() {
3950
for (Letter letter : lettersToComplete) {
4051
letter.updateStatus(Status.DELIVERED);
4152
log.info("편지 ID: {} 배송 완료 처리됨", letter.getId());
53+
// 도착 알림 전송
54+
notificationFacade.sendNotification(
55+
senderZipCodes.get(letter.getWriterId()),
56+
letter.getReceiverId(),
57+
AlarmType.LETTER,
58+
letter.getId().toString());
4259
}
4360

4461
// 변경사항 저장

src/test/java/io/crops/warmletter/domain/letter/service/LetterServiceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ void writeDirectLetter_success() {
406406
);
407407
//verify 메서드로 letterRepository.save() 메서드가 정확히 1번 호출되었는지 확인
408408
verify(letterRepository).save(any(Letter.class));
409-
verify(notificationFacade).sendNotification(anyString(), anyLong(), any(), anyString());
409+
verify(notificationFacade).sendNotification(anyString(), anyLong(), any(), eq(null));
410410
}
411411

412412

0 commit comments

Comments
 (0)