Skip to content

Commit bfe3c5e

Browse files
authored
Merge pull request #239 from prgrms-web-devcourse-final-project/refactor/alarm(WR9-139)
Refactor/alarm(wr9 139)
2 parents 19c5352 + cda56e9 commit bfe3c5e

File tree

13 files changed

+129
-74
lines changed

13 files changed

+129
-74
lines changed

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import io.crops.warmletter.domain.letter.dto.request.CreateLetterRequest;
66
import io.crops.warmletter.domain.letter.dto.request.EvaluateLetterRequest;
77
import io.crops.warmletter.domain.letter.dto.request.TemporarySaveLetterRequest;
8-
import io.crops.warmletter.domain.letter.dto.response.LetterDraftResponse;
98
import io.crops.warmletter.domain.letter.dto.response.LetterResponse;
109
import io.crops.warmletter.domain.letter.entity.Letter;
1110
import io.crops.warmletter.domain.letter.entity.LetterMatching;
@@ -17,17 +16,17 @@
1716
import io.crops.warmletter.domain.member.exception.MemberNotFoundException;
1817
import io.crops.warmletter.domain.member.facade.MemberFacade;
1918
import io.crops.warmletter.domain.member.repository.MemberRepository;
19+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
2020
import io.crops.warmletter.domain.timeline.enums.AlarmType;
21-
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
2221
import io.crops.warmletter.global.error.exception.BusinessException;
2322
import lombok.RequiredArgsConstructor;
23+
import org.springframework.context.ApplicationEventPublisher;
2424
import org.springframework.stereotype.Service;
2525
import org.springframework.transaction.annotation.Transactional;
2626

2727
import java.util.ArrayList;
2828
import java.util.List;
2929
import java.util.Map;
30-
import java.util.Optional;
3130
import java.util.stream.Collectors;
3231

3332
import static io.crops.warmletter.global.error.common.ErrorCode.INVALID_INPUT_VALUE;
@@ -44,7 +43,7 @@ public class LetterService {
4443
private final MemberFacade memberFacade;
4544
private final AuthFacade authFacade;
4645

47-
private final NotificationFacade notificationFacade;
46+
private final ApplicationEventPublisher notificationPublisher;
4847

4948
@Transactional
5049
public LetterResponse createLetter(CreateLetterRequest request) {
@@ -109,7 +108,12 @@ public LetterResponse createLetter(CreateLetterRequest request) {
109108

110109
// 알림 전송
111110
if(request.getReceiverId() != null){
112-
notificationFacade.sendNotification(zipCode,request.getReceiverId(), AlarmType.SENDING,null);
111+
notificationPublisher.publishEvent(NotificationRequest.builder()
112+
.senderZipCode(zipCode)
113+
.receiverId(request.getReceiverId())
114+
.alarmType(AlarmType.SENDING)
115+
.data(null)
116+
.build());
113117
}
114118

115119
return LetterResponse.fromEntity(savedLetter, zipCode);

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import io.crops.warmletter.domain.share.exception.ShareProposalNotFoundException;
2929
import io.crops.warmletter.domain.share.repository.SharePostRepository;
3030
import io.crops.warmletter.domain.share.repository.ShareProposalRepository;
31+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
3132
import io.crops.warmletter.domain.timeline.enums.AlarmType;
3233
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
3334
import jakarta.transaction.Transactional;
3435
import lombok.RequiredArgsConstructor;
36+
import org.springframework.context.ApplicationEventPublisher;
3537
import org.springframework.data.domain.Page;
3638
import org.springframework.data.domain.Pageable;
3739
import org.springframework.stereotype.Service;
@@ -56,7 +58,8 @@ public class ReportService {
5658
private final ReportModerationService reportModerationService;
5759

5860
private final AuthFacade authFacde;
59-
private final NotificationFacade notificationFacade;
61+
62+
private final ApplicationEventPublisher notificationPublisher;
6063

6164
@Transactional
6265
public UpdateReportResponse updateReport(Long reportId, UpdateReportRequest request) {
@@ -81,7 +84,12 @@ public UpdateReportResponse updateReport(Long reportId, UpdateReportRequest requ
8184
}
8285
resolvePendingReports(report);
8386
// targetMemberId로 알림 전송
84-
notificationFacade.sendNotification(null, targetMemberId, AlarmType.REPORT, report.getAdminMemo()+"§"+reportedMember.getWarningCount());
87+
notificationPublisher.publishEvent(NotificationRequest.builder()
88+
.senderZipCode(null)
89+
.receiverId(targetMemberId)
90+
.alarmType(AlarmType.REPORT)
91+
.data(report.getAdminMemo()+"§"+reportedMember.getWarningCount())
92+
.build());
8593
}
8694
return new UpdateReportResponse(report,reportedMember);
8795
}
@@ -142,7 +150,13 @@ public void updateReportWithAIResult(Long reportId, Map<String, String> moderati
142150
.orElseThrow(MemberNotFoundException::new);
143151
reportedMember.increaseWarningCount();
144152
memberRepository.save(reportedMember);
145-
notificationFacade.sendNotification(null, targetMemberId, AlarmType.REPORT, report.getAdminMemo()+"§"+reportedMember.getWarningCount());
153+
// 알림
154+
notificationPublisher.publishEvent(NotificationRequest.builder()
155+
.senderZipCode(null)
156+
.receiverId(targetMemberId)
157+
.alarmType(AlarmType.REPORT)
158+
.data(report.getAdminMemo()+"§"+reportedMember.getWarningCount())
159+
.build());
146160
}
147161
resolvePendingReports(report);
148162
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
package io.crops.warmletter.domain.share.repository;
22
import io.crops.warmletter.domain.share.entity.ShareProposal;
33
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Query;
45
import org.springframework.stereotype.Repository;
56

67
@Repository
78
public interface ShareProposalRepository extends JpaRepository<ShareProposal,Long >, ShareProposalRepositoryCustom {
8-
9+
@Query("SELECT m.zipCode " +
10+
"FROM ShareProposal proposal " +
11+
"JOIN Member m ON proposal.requesterId = m.id " +
12+
"WHERE proposal.requesterId = :requesterId")
913
String findZipCodeByRequesterId(Long requesterId);
14+
15+
@Query("SELECT m.zipCode " +
16+
"FROM ShareProposal proposal " +
17+
"JOIN Member m ON proposal.recipientId = m.id " +
18+
"WHERE proposal.recipientId = :recipientId")
19+
String findZipCodeByRecipientId(Long recipientId);
1020
}

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import io.crops.warmletter.domain.share.exception.ShareAccessException;
1313
import io.crops.warmletter.domain.share.exception.ShareProposalNotFoundException;
1414
import io.crops.warmletter.domain.share.repository.*;
15+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
1516
import io.crops.warmletter.domain.timeline.enums.AlarmType;
1617
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
1718
import lombok.RequiredArgsConstructor;
19+
import org.springframework.context.ApplicationEventPublisher;
1820
import org.springframework.stereotype.Service;
1921
import org.springframework.transaction.annotation.Transactional;
2022
import java.util.List;
@@ -29,7 +31,7 @@ public class ShareProposalService {
2931
private final SharePostRepository sharePostRepository;
3032
private final AuthFacade authFacade;
3133

32-
private final NotificationFacade notificationFacade;
34+
private final ApplicationEventPublisher notificationPublisher;
3335

3436
@Transactional
3537
public ShareProposalResponse requestShareProposal(ShareProposalRequest request) {
@@ -48,15 +50,18 @@ public ShareProposalResponse requestShareProposal(ShareProposalRequest request)
4850
throw new ShareProposalNotFoundException();
4951
}
5052
// 알림 전송
51-
notificationFacade.sendNotification(response.getZipCode(), request.getRecipientId(), AlarmType.SHARE, response.getShareProposalId().toString());
53+
notificationPublisher.publishEvent(NotificationRequest.builder()
54+
.senderZipCode(response.getZipCode())
55+
.receiverId(request.getRecipientId())
56+
.alarmType(AlarmType.SHARE)
57+
.data(response.getShareProposalId().toString())
58+
.build());
5259
return response;
5360
}
5461

5562
@Transactional
5663
public ShareProposalStatusResponse approveShareProposal(Long shareProposalId) {
57-
5864
Long memberId = authFacade.getCurrentUserId();
59-
6065
ShareProposal shareProposal = shareProposalRepository.findById(shareProposalId)
6166
.orElseThrow(() -> new ShareProposalNotFoundException());
6267

@@ -73,11 +78,21 @@ public ShareProposalStatusResponse approveShareProposal(Long shareProposalId) {
7378
.isActive(true)
7479
.build();
7580
sharePost = sharePostRepository.save(sharePost);
76-
// 알림 전송(양쪽다)
81+
// 알림 전송(양쪽 다)
7782
String requestZipCode = shareProposalRepository.findZipCodeByRequesterId(shareProposal.getRequesterId());
78-
String recipientZipCode = shareProposalRepository.findZipCodeByRequesterId(shareProposal.getRecipientId());
79-
notificationFacade.sendNotification(recipientZipCode, shareProposal.getRequesterId(), AlarmType.POSTED, sharePost.getId().toString());
80-
notificationFacade.sendNotification(requestZipCode, shareProposal.getRecipientId(), AlarmType.POSTED, sharePost.getId().toString());
83+
String recipientZipCode = shareProposalRepository.findZipCodeByRecipientId(shareProposal.getRecipientId());
84+
notificationPublisher.publishEvent(NotificationRequest.builder()
85+
.senderZipCode(recipientZipCode)
86+
.receiverId(shareProposal.getRequesterId())
87+
.alarmType(AlarmType.POSTED)
88+
.data(sharePost.getId().toString())
89+
.build());
90+
notificationPublisher.publishEvent(NotificationRequest.builder()
91+
.senderZipCode(requestZipCode)
92+
.receiverId(shareProposal.getRecipientId())
93+
.alarmType(AlarmType.POSTED)
94+
.data(sharePost.getId().toString())
95+
.build());
8196
return ShareProposalStatusResponse.builder()
8297
.shareProposalId(shareProposal.getId())
8398
.status(shareProposal.getStatus())
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.crops.warmletter.domain.timeline.dto.request;
2+
3+
import io.crops.warmletter.domain.timeline.enums.AlarmType;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Builder
9+
public class NotificationRequest {
10+
String senderZipCode;
11+
Long receiverId;
12+
AlarmType alarmType;
13+
String data;
14+
}
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package io.crops.warmletter.domain.timeline.facade;
22

3+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
34
import io.crops.warmletter.domain.timeline.enums.AlarmType;
45
import io.crops.warmletter.domain.timeline.service.NotificationService;
56
import lombok.RequiredArgsConstructor;
7+
import org.springframework.scheduling.annotation.Async;
68
import org.springframework.stereotype.Component;
9+
import org.springframework.transaction.event.TransactionPhase;
10+
import org.springframework.transaction.event.TransactionalEventListener;
711

812
@Component
913
@RequiredArgsConstructor
1014
public class NotificationFacade {
1115

1216
private final NotificationService notificationService;
1317

14-
public void sendNotification(String senderZipCode, Long receiverId, AlarmType alarmType, String data) {
15-
notificationService.createNotification(senderZipCode, receiverId, alarmType, data);
18+
@Async
19+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
20+
public void sendNotification(NotificationRequest notificationRequest) {
21+
notificationService.createNotification(
22+
notificationRequest.getSenderZipCode(),
23+
notificationRequest.getReceiverId(),
24+
notificationRequest.getAlarmType(),
25+
notificationRequest.getData());
1626
}
1727
}

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

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,7 @@ public class NotificationService {
2727
private final TimelineRepository timelineRepository;
2828

2929
public SseEmitter subscribeNotification(){
30-
Long memberId;
31-
try {
32-
memberId = authFacade.getCurrentUserId();
33-
} catch (UnauthorizedException e){
34-
log.warn("SSE 구독 실패: 인증되지 않은 사용자");
35-
SseEmitter emitter = new SseEmitter(0L);
36-
NotificationResponse notificationResponse = NotificationResponse.builder()
37-
.title("Unauthorized")
38-
.alarmType("TEST").build();
39-
try{
40-
emitter.send(SseEmitter.event()
41-
.data(notificationResponse));
42-
}catch (IOException ioException){
43-
log.warn("SSE 에러 전송 실패 - Unauthorized");
44-
}
45-
emitter.complete();
46-
return emitter;
47-
}
30+
Long memberId = authFacade.getCurrentUserId();
4831

4932
SseEmitter emitter = new SseEmitter(600_000L); // 10분 후 타임아웃 설정
5033

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import io.crops.warmletter.domain.letter.enums.Status;
55
import io.crops.warmletter.domain.letter.repository.LetterRepository;
66
import io.crops.warmletter.domain.member.repository.MemberRepository;
7+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
78
import io.crops.warmletter.domain.timeline.dto.response.LetterAlarmResponse;
89
import io.crops.warmletter.domain.timeline.enums.AlarmType;
910
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
1011
import jakarta.transaction.Transactional;
1112
import lombok.RequiredArgsConstructor;
1213
import lombok.extern.slf4j.Slf4j;
14+
import org.springframework.context.ApplicationEventPublisher;
1315
import org.springframework.context.annotation.Configuration;
1416
import org.springframework.scheduling.annotation.Scheduled;
1517

@@ -25,7 +27,8 @@
2527
public class DeliverySchedule {
2628

2729
private final LetterRepository letterRepository;
28-
private final NotificationFacade notificationFacade;
30+
31+
private final ApplicationEventPublisher notificationPublisher;
2932

3033
@Transactional
3134
@Scheduled(cron = "0 */1 * * * *", zone = "Asia/Seoul")
@@ -55,11 +58,12 @@ public void processDeliveryCompletion() {
5558
letter.updateStatus(Status.DELIVERED);
5659
log.info("편지 ID: {} 배송 완료 처리됨", letter.getId());
5760
// 도착 알림 전송
58-
notificationFacade.sendNotification(
59-
senderZipCodes.get(letter.getWriterId()),
60-
letter.getReceiverId(),
61-
AlarmType.LETTER,
62-
letter.getId().toString());
61+
notificationPublisher.publishEvent(NotificationRequest.builder()
62+
.senderZipCode(senderZipCodes.get(letter.getWriterId()))
63+
.receiverId(letter.getReceiverId())
64+
.alarmType(AlarmType.LETTER)
65+
.data(letter.getId().toString())
66+
.build());
6367
}
6468

6569
// 변경사항 저장

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.crops.warmletter.domain.member.enums.Role;
1818
import io.crops.warmletter.domain.member.facade.MemberFacade;
1919
import io.crops.warmletter.domain.member.repository.MemberRepository;
20+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
2021
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
2122
import org.junit.jupiter.api.BeforeEach;
2223
import org.junit.jupiter.api.DisplayName;
@@ -25,6 +26,7 @@
2526
import org.mockito.InjectMocks;
2627
import org.mockito.Mock;
2728
import org.mockito.junit.jupiter.MockitoExtension;
29+
import org.springframework.context.ApplicationEventPublisher;
2830
import org.springframework.test.util.ReflectionTestUtils;
2931

3032
import java.lang.reflect.Field;
@@ -61,6 +63,9 @@ class LetterServiceTest {
6163
@Mock
6264
private NotificationFacade notificationFacade;
6365

66+
@Mock
67+
private ApplicationEventPublisher notificationPublisher;
68+
6469
@InjectMocks
6570
private LetterService letterService;
6671

@@ -406,7 +411,7 @@ void writeDirectLetter_success() {
406411
);
407412
//verify 메서드로 letterRepository.save() 메서드가 정확히 1번 호출되었는지 확인
408413
verify(letterRepository).save(any(Letter.class));
409-
verify(notificationFacade).sendNotification(anyString(), anyLong(), any(), eq(null));
414+
verify(notificationPublisher).publishEvent(any(NotificationRequest.class));
410415
}
411416

412417
@Test
@@ -479,7 +484,7 @@ void createLetter_deletesSavedLetterWhenLetterIdProvided() {
479484
// then
480485
verify(letterRepository).delete(temporarySavedLetter);
481486
verify(letterRepository).save(any(Letter.class));
482-
487+
verify(notificationPublisher).publishEvent(any(NotificationRequest.class));
483488
// 응답 검증
484489
assertAll("편지 응답 검증",
485490
() -> assertNotNull(response),
@@ -557,7 +562,7 @@ void createLetter_doesNotDeleteNonSavedLetter() {
557562
// then
558563
verify(letterRepository, never()).delete(nonSavedLetter);
559564
verify(letterRepository).save(any(Letter.class));
560-
565+
verify(notificationPublisher).publishEvent(any(NotificationRequest.class));
561566
// 응답 검증
562567
assertAll("편지 응답 검증",
563568
() -> assertNotNull(response),

0 commit comments

Comments
 (0)