Skip to content

Commit dcf61b0

Browse files
authored
Feature/189 봉사 후기 요청 이벤트, 알림 (#203)
* refactor(VolunteerApplyStatusChangeEvent): 패키지 이동 * refactor(VolunteerApplyStatusChangeEvent): 인자, 파라미터 정리 - oldStatus 추가. - 이벤트에 receiverId가 포함되는 것이 어색, volunteerId로 수정. * feat(VolunteerReviewRequestEvent): 봉사 후기 요청 이벤트 추가 * feat(VolunteerReviewRequestEvent): 봉사 후기 요청 이벤트 발행 추가 - SettleVolunteerApplyFacadeService 에서 이벤트 발생. - subType 명명 수정. * feat(NotificationHandlerImpl): 트랜잭션 추가 - DB save 작업 * feat(MessageConverter): buildVolunteerReviewRequestNotification 추가 - buildVolunteerApplyStatusChangeNotification 와 형태가 비슷해서 리팩토링 예정. - createVolunteerApplyStatusChangeNotificationTitle dafault를 명확하게 예외로 처리. * fix(event): JsonProperty와 필드 명 맞추기 * test(NotificationHandler): createdAt null check 추가 * feat(event): 역직렬화 필수 필드 설정 * test(MessageConverter): 메시지 컨버팅 테스트 추가 * test(NotificationHandler): 메시지 수정 * refactor: 예외 명시
1 parent fdd7420 commit dcf61b0

File tree

12 files changed

+214
-36
lines changed

12 files changed

+214
-36
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.somemore.facade.event;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.somemore.global.common.event.ServerEvent;
6+
import com.somemore.global.common.event.ServerEventType;
7+
import com.somemore.notification.domain.NotificationSubType;
8+
import lombok.Getter;
9+
import lombok.experimental.SuperBuilder;
10+
11+
import java.time.LocalDateTime;
12+
import java.util.UUID;
13+
14+
@Getter
15+
@SuperBuilder
16+
public class VolunteerReviewRequestEvent extends ServerEvent<NotificationSubType> {
17+
private final UUID volunteerId;
18+
private final Long volunteerApplyId;
19+
private final UUID centerId;
20+
private final Long recruitBoardId;
21+
22+
@JsonCreator
23+
public VolunteerReviewRequestEvent(
24+
@JsonProperty(value = "volunteerId", required = true) UUID volunteerId,
25+
@JsonProperty(value = "volunteerApplyId", required = true) Long volunteerApplyId,
26+
@JsonProperty(value = "centerId", required = true) UUID centerId,
27+
@JsonProperty(value = "recruitBoardId", required = true) Long recruitBoardId
28+
) {
29+
super(ServerEventType.NOTIFICATION, NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE, LocalDateTime.now());
30+
this.volunteerId = volunteerId;
31+
this.volunteerApplyId = volunteerApplyId;
32+
this.centerId = centerId;
33+
this.recruitBoardId = recruitBoardId;
34+
}
35+
}

src/main/java/com/somemore/facade/volunteerapply/SettleVolunteerApplyFacadeService.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD;
55
import static com.somemore.global.exception.ExceptionMessage.VOLUNTEER_APPLY_LIST_MISMATCH;
66

7+
import com.somemore.facade.event.VolunteerReviewRequestEvent;
8+
import com.somemore.global.common.event.ServerEventPublisher;
9+
import com.somemore.global.common.event.ServerEventType;
710
import com.somemore.global.exception.BadRequestException;
11+
import com.somemore.notification.domain.NotificationSubType;
812
import com.somemore.recruitboard.domain.RecruitBoard;
913
import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase;
1014
import com.somemore.volunteer.usecase.UpdateVolunteerUseCase;
@@ -25,6 +29,7 @@ public class SettleVolunteerApplyFacadeService implements SettleVolunteerApplyFa
2529
private final VolunteerApplyQueryUseCase volunteerApplyQueryUseCase;
2630
private final RecruitBoardQueryUseCase recruitBoardQueryUseCase;
2731
private final UpdateVolunteerUseCase updateVolunteerUseCase;
32+
private final ServerEventPublisher serverEventPublisher;
2833

2934
@Override
3035
public void settleVolunteerApplies(VolunteerApplySettleRequestDto dto, UUID centerId) {
@@ -41,6 +46,7 @@ public void settleVolunteerApplies(VolunteerApplySettleRequestDto dto, UUID cent
4146
applies.forEach(apply -> {
4247
apply.changeAttended(true);
4348
updateVolunteerUseCase.updateVolunteerStats(apply.getVolunteerId(), hours);
49+
publishVolunteerReviewRequestEvent(apply, recruitBoard);
4450
});
4551

4652
}
@@ -68,4 +74,17 @@ private void validateRecruitBoardConsistency(List<VolunteerApply> applies,
6874
throw new BadRequestException(RECRUIT_BOARD_ID_MISMATCH);
6975
}
7076
}
77+
78+
private void publishVolunteerReviewRequestEvent(VolunteerApply apply, RecruitBoard recruitBoard) {
79+
VolunteerReviewRequestEvent event = VolunteerReviewRequestEvent.builder()
80+
.type(ServerEventType.NOTIFICATION)
81+
.subType(NotificationSubType.VOLUNTEER_REVIEW_REQUEST)
82+
.volunteerId(apply.getVolunteerId())
83+
.volunteerApplyId(apply.getId())
84+
.centerId(recruitBoard.getCenterId())
85+
.recruitBoardId(recruitBoard.getId())
86+
.build();
87+
88+
serverEventPublisher.publish(event);
89+
}
7190
}

src/main/java/com/somemore/global/common/event/ServerEvent.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ public abstract class ServerEvent<T extends Enum<T>> {
2020
private final LocalDateTime createdAt;
2121

2222
protected ServerEvent(
23-
@JsonProperty("type") ServerEventType type,
24-
@JsonProperty("subType") T subType,
25-
@JsonProperty("createdAt") LocalDateTime createdAt
23+
@JsonProperty(value = "type", required = true) ServerEventType type,
24+
@JsonProperty(value = "subType", required = true) T subType,
25+
@JsonProperty(value = "createdAt", required = true) LocalDateTime createdAt
2626
) {
2727
this.type = type;
2828
this.subType = subType;
Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.somemore.notification.converter;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonMappingException;
35
import com.fasterxml.jackson.databind.JsonNode;
46
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.somemore.facade.event.VolunteerReviewRequestEvent;
58
import com.somemore.notification.domain.Notification;
69
import com.somemore.notification.domain.NotificationSubType;
710
import com.somemore.volunteerapply.domain.ApplyStatus;
8-
import com.somemore.volunteerapply.domain.VolunteerApplyStatusChangeEvent;
11+
import com.somemore.volunteerapply.event.VolunteerApplyStatusChangeEvent;
912
import lombok.RequiredArgsConstructor;
1013
import lombok.extern.slf4j.Slf4j;
1114
import org.springframework.stereotype.Component;
@@ -24,7 +27,7 @@ public Notification from(String message) {
2427

2528
return switch (NotificationSubType.from(eventType)) {
2629
case NOTE_BLAH_BLAH -> throw new UnsupportedOperationException("NOTE 알림 타입 처리 로직 미구현");
27-
case REVIEW_BLAH_BLAH -> throw new UnsupportedOperationException("REVIEW 알림 타입 처리 로직 미구현");
30+
case VOLUNTEER_REVIEW_REQUEST -> buildVolunteerReviewRequestNotification(message);
2831
case VOLUNTEER_APPLY_STATUS_CHANGE -> buildVolunteerApplyStatusChangeNotification(message);
2932
};
3033
} catch (Exception e) {
@@ -33,32 +36,40 @@ public Notification from(String message) {
3336
}
3437
}
3538

36-
private Notification buildVolunteerApplyStatusChangeNotification(String message) throws Exception {
37-
VolunteerApplyStatusChangeEvent event = objectMapper.readValue(message, VolunteerApplyStatusChangeEvent.class);
39+
private Notification buildVolunteerReviewRequestNotification(String message) throws JsonProcessingException {
40+
VolunteerReviewRequestEvent event = objectMapper.readValue(message, VolunteerReviewRequestEvent.class);
3841

3942
return Notification.builder()
40-
.receiverId(event.getReceiverId())
41-
.title(buildNotificationTitle(event.getNewStatus()))
42-
.type(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE)
43+
.receiverId(event.getVolunteerId())
44+
.title(createVolunteerReviewRequestNotificationTitle())
45+
.type(NotificationSubType.VOLUNTEER_REVIEW_REQUEST)
4346
.relatedId(event.getRecruitBoardId())
4447
.build();
4548
}
4649

47-
private Notification handleNoteEvent(String message) {
48-
// TODO: NOTE 이벤트를 처리하는 로직 구현
49-
throw new UnsupportedOperationException("NOTE 알림 타입 처리 로직 미구현");
50+
private Notification buildVolunteerApplyStatusChangeNotification(String message) throws JsonProcessingException {
51+
VolunteerApplyStatusChangeEvent event = objectMapper.readValue(message, VolunteerApplyStatusChangeEvent.class);
52+
53+
return Notification.builder()
54+
.receiverId(event.getVolunteerId())
55+
.title(createVolunteerApplyStatusChangeNotificationTitle(event.getNewStatus()))
56+
.type(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE)
57+
.relatedId(event.getRecruitBoardId())
58+
.build();
5059
}
5160

52-
private Notification handleReviewEvent(String message) {
53-
// TODO: System 이벤트를 처리하는 로직 구현
54-
throw new UnsupportedOperationException("REVIEW 알림 타입 처리 로직 미구현");
61+
private String createVolunteerReviewRequestNotificationTitle() {
62+
return "최근 활동하신 활동의 후기를 작성해 주세요!";
5563
}
5664

57-
private String buildNotificationTitle(ApplyStatus newStatus) {
65+
private String createVolunteerApplyStatusChangeNotificationTitle(ApplyStatus newStatus) {
5866
return switch (newStatus) {
5967
case APPROVED -> "봉사 활동 신청이 승인되었습니다.";
6068
case REJECTED -> "봉사 활동 신청이 거절되었습니다.";
61-
default -> "봉사 활동 신청 상태가 변경되었습니다.";
69+
default -> {
70+
log.error("올바르지 않은 봉사 신청 상태입니다: {}", newStatus);
71+
throw new IllegalArgumentException();
72+
}
6273
};
6374
}
6475
}

src/main/java/com/somemore/notification/domain/NotificationSubType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
@RequiredArgsConstructor
88
public enum NotificationSubType {
99
NOTE_BLAH_BLAH("쪽지"),
10-
REVIEW_BLAH_BLAH("후기 요청"),
10+
VOLUNTEER_REVIEW_REQUEST("봉사 후기 요청"),
1111
VOLUNTEER_APPLY_STATUS_CHANGE("신청 상태 변경")
1212
;
1313

src/main/java/com/somemore/notification/handler/NotificationHandlerImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
import com.somemore.sse.usecase.SseUseCase;
99
import lombok.RequiredArgsConstructor;
1010
import org.springframework.stereotype.Component;
11+
import org.springframework.transaction.annotation.Transactional;
1112

1213
@Component
1314
@RequiredArgsConstructor
15+
@Transactional
1416
public class NotificationHandlerImpl implements NotificationHandler {
1517

1618
private final NotificationRepository notificationRepository;

src/main/java/com/somemore/volunteerapply/domain/VolunteerApplyStatusChangeEvent.java renamed to src/main/java/com/somemore/volunteerapply/event/VolunteerApplyStatusChangeEvent.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
package com.somemore.volunteerapply.domain;
1+
package com.somemore.volunteerapply.event;
22

33
import com.fasterxml.jackson.annotation.JsonCreator;
44
import com.fasterxml.jackson.annotation.JsonProperty;
55
import com.somemore.global.common.event.ServerEvent;
66
import com.somemore.global.common.event.ServerEventType;
77
import com.somemore.notification.domain.NotificationSubType;
8+
import com.somemore.volunteerapply.domain.ApplyStatus;
89
import lombok.Getter;
910
import lombok.experimental.SuperBuilder;
1011

@@ -15,7 +16,7 @@
1516
@SuperBuilder
1617
public class VolunteerApplyStatusChangeEvent extends ServerEvent<NotificationSubType> {
1718

18-
private final UUID receiverId;
19+
private final UUID volunteerId;
1920
private final Long volunteerApplyId;
2021
private final UUID centerId;
2122
private final Long recruitBoardId;
@@ -24,15 +25,15 @@ public class VolunteerApplyStatusChangeEvent extends ServerEvent<NotificationSub
2425

2526
@JsonCreator
2627
public VolunteerApplyStatusChangeEvent(
27-
@JsonProperty("receiverId") UUID receiverId,
28-
@JsonProperty("volunteerApplyId") Long volunteerApplyId,
29-
@JsonProperty("centerId") UUID centerId,
30-
@JsonProperty("recruitBoardId") Long recruitBoardId,
31-
@JsonProperty("oldStatus") ApplyStatus oldStatus,
32-
@JsonProperty("newStatus") ApplyStatus newStatus
28+
@JsonProperty(value = "volunteerId", required = true) UUID volunteerId,
29+
@JsonProperty(value = "volunteerApplyId", required = true) Long volunteerApplyId,
30+
@JsonProperty(value = "centerId", required = true) UUID centerId,
31+
@JsonProperty(value = "recruitBoardId", required = true) Long recruitBoardId,
32+
@JsonProperty(value = "oldStatus", required = true) ApplyStatus oldStatus,
33+
@JsonProperty(value = "newStatus", required = true) ApplyStatus newStatus
3334
) {
3435
super(ServerEventType.NOTIFICATION, NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE, LocalDateTime.now());
35-
this.receiverId = receiverId;
36+
this.volunteerId = volunteerId;
3637
this.volunteerApplyId = volunteerApplyId;
3738
this.centerId = centerId;
3839
this.recruitBoardId = recruitBoardId;

src/main/java/com/somemore/volunteerapply/service/ApproveVolunteerApplyService.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import com.somemore.notification.domain.NotificationSubType;
77
import com.somemore.recruitboard.domain.RecruitBoard;
88
import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase;
9+
import com.somemore.volunteerapply.domain.ApplyStatus;
910
import com.somemore.volunteerapply.domain.VolunteerApply;
10-
import com.somemore.volunteerapply.domain.VolunteerApplyStatusChangeEvent;
11+
import com.somemore.volunteerapply.event.VolunteerApplyStatusChangeEvent;
1112
import com.somemore.volunteerapply.repository.VolunteerApplyRepository;
1213
import com.somemore.volunteerapply.usecase.ApproveVolunteerApplyUseCase;
1314
import lombok.RequiredArgsConstructor;
@@ -38,10 +39,11 @@ public void approve(Long id, UUID centerId) {
3839
validateWriter(recruitBoard, centerId);
3940
validateBoardStatus(recruitBoard);
4041

42+
ApplyStatus oldStatus = apply.getStatus();
4143
apply.changeStatus(APPROVED);
4244
volunteerApplyRepository.save(apply);
4345

44-
publishVolunteerApplyStatusChangeEvent(apply.getVolunteerId(), id, recruitBoard, apply);
46+
publishVolunteerApplyStatusChangeEvent(apply, recruitBoard, oldStatus);
4547
}
4648

4749
private VolunteerApply getVolunteerApply(Long id) {
@@ -63,14 +65,15 @@ private void validateBoardStatus(RecruitBoard recruitBoard) {
6365
}
6466
}
6567

66-
private void publishVolunteerApplyStatusChangeEvent(UUID receiverId, Long id, RecruitBoard recruitBoard, VolunteerApply apply) {
68+
private void publishVolunteerApplyStatusChangeEvent(VolunteerApply apply, RecruitBoard recruitBoard, ApplyStatus oldStatus) {
6769
VolunteerApplyStatusChangeEvent event = VolunteerApplyStatusChangeEvent.builder()
6870
.type(ServerEventType.NOTIFICATION)
6971
.subType(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE)
70-
.receiverId(receiverId)
71-
.volunteerApplyId(id)
72+
.volunteerId(apply.getVolunteerId())
73+
.volunteerApplyId(apply.getId())
7274
.centerId(recruitBoard.getCenterId())
7375
.recruitBoardId(recruitBoard.getId())
76+
.oldStatus(oldStatus)
7477
.newStatus(apply.getStatus())
7578
.build();
7679

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.somemore.notification.converter;
2+
3+
import com.somemore.IntegrationTestSupport;
4+
import com.somemore.notification.domain.Notification;
5+
import com.somemore.notification.domain.NotificationSubType;
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
10+
import java.util.UUID;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.junit.jupiter.api.Assertions.assertThrows;
14+
15+
class MessageConverterTest extends IntegrationTestSupport {
16+
17+
@Autowired
18+
private MessageConverter messageConverter;
19+
20+
@Test
21+
@DisplayName("VOLUNTEER_REVIEW_REQUEST 메시지를 변환하면 Notification 객체를 반환한다")
22+
void testVolunteerReviewRequestConversion() {
23+
// given
24+
String message = """
25+
{
26+
"type": "NOTIFICATION",
27+
"subType": "VOLUNTEER_REVIEW_REQUEST",
28+
"volunteerId": "123e4567-e89b-12d3-a456-426614174000",
29+
"volunteerApplyId": "1",
30+
"centerId": "123e4567-e89b-12d3-a456-426614174001",
31+
"recruitBoardId": 456,
32+
"createdAt": "2024-12-05T10:00:00"
33+
}
34+
""";
35+
36+
// when
37+
Notification notification = messageConverter.from(message);
38+
39+
// then
40+
assertThat(notification.getReceiverId()).isEqualTo(UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
41+
assertThat(notification.getTitle()).isEqualTo("최근 활동하신 활동의 후기를 작성해 주세요!");
42+
assertThat(notification.getType()).isEqualTo(NotificationSubType.VOLUNTEER_REVIEW_REQUEST);
43+
assertThat(notification.getRelatedId()).isEqualTo(456L);
44+
}
45+
46+
@Test
47+
@DisplayName("임의의 필드가 추가된 VOLUNTEER_APPLY_STATUS_CHANGE 메시지를 변환해도 Notification 객체를 반환한다")
48+
void testVolunteerApplyStatusChangeConversion() {
49+
// given
50+
String message = """
51+
{
52+
"extraField": "this should be ignored",
53+
"extraField": "this should be ignored",
54+
"extraField": "this should be ignored",
55+
"extraField": "this should be ignored",
56+
"extraField": "this should be ignored",
57+
"type": "NOTIFICATION",
58+
"subType": "VOLUNTEER_APPLY_STATUS_CHANGE",
59+
"volunteerId": "123e4567-e89b-12d3-a456-426614174000",
60+
"centerId": "123e4567-e89b-12d3-a456-426614174001",
61+
"volunteerApplyId": "1",
62+
"recruitBoardId": 456,
63+
"oldStatus": "WAITING",
64+
"newStatus": "APPROVED",
65+
"createdAt": "2024-12-05T10:00:00"
66+
}
67+
""";
68+
69+
// when
70+
Notification notification = messageConverter.from(message);
71+
72+
// then
73+
assertThat(notification.getReceiverId()).isEqualTo(UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
74+
assertThat(notification.getTitle()).isEqualTo("봉사 활동 신청이 승인되었습니다.");
75+
assertThat(notification.getType()).isEqualTo(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE);
76+
assertThat(notification.getRelatedId()).isEqualTo(456L);
77+
}
78+
79+
@Test
80+
@DisplayName("잘못된 JSON 메시지를 변환하면 IllegalStateException을 던진다")
81+
void testInvalidJson() {
82+
// given
83+
String invalidMessage = "{ invalid-json }";
84+
85+
// when
86+
// then
87+
assertThrows(IllegalStateException.class, () -> messageConverter.from(invalidMessage));
88+
}
89+
90+
@Test
91+
@DisplayName("필수 필드가 누락된 메시지를 변환하면 IllegalStateException을 던진다")
92+
void testMissingFields() {
93+
// given
94+
String messageWithMissingFields = """
95+
{
96+
"type": "NOTIFICATION",
97+
"subType": "VOLUNTEER_REVIEW_REQUEST"
98+
}
99+
""";
100+
101+
// when & then
102+
assertThrows(IllegalStateException.class, () -> messageConverter.from(messageWithMissingFields));
103+
}
104+
}

0 commit comments

Comments
 (0)