diff --git a/src/main/java/com/somemore/facade/event/VolunteerReviewRequestEvent.java b/src/main/java/com/somemore/facade/event/VolunteerReviewRequestEvent.java new file mode 100644 index 000000000..2e7e43a82 --- /dev/null +++ b/src/main/java/com/somemore/facade/event/VolunteerReviewRequestEvent.java @@ -0,0 +1,35 @@ +package com.somemore.facade.event; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.somemore.global.common.event.ServerEvent; +import com.somemore.global.common.event.ServerEventType; +import com.somemore.notification.domain.NotificationSubType; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@SuperBuilder +public class VolunteerReviewRequestEvent extends ServerEvent { + private final UUID volunteerId; + private final Long volunteerApplyId; + private final UUID centerId; + private final Long recruitBoardId; + + @JsonCreator + public VolunteerReviewRequestEvent( + @JsonProperty(value = "volunteerId", required = true) UUID volunteerId, + @JsonProperty(value = "volunteerApplyId", required = true) Long volunteerApplyId, + @JsonProperty(value = "centerId", required = true) UUID centerId, + @JsonProperty(value = "recruitBoardId", required = true) Long recruitBoardId + ) { + super(ServerEventType.NOTIFICATION, NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE, LocalDateTime.now()); + this.volunteerId = volunteerId; + this.volunteerApplyId = volunteerApplyId; + this.centerId = centerId; + this.recruitBoardId = recruitBoardId; + } +} diff --git a/src/main/java/com/somemore/facade/volunteerapply/SettleVolunteerApplyFacadeService.java b/src/main/java/com/somemore/facade/volunteerapply/SettleVolunteerApplyFacadeService.java index f2c25749f..d6cfb6d4e 100644 --- a/src/main/java/com/somemore/facade/volunteerapply/SettleVolunteerApplyFacadeService.java +++ b/src/main/java/com/somemore/facade/volunteerapply/SettleVolunteerApplyFacadeService.java @@ -4,7 +4,11 @@ import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD; import static com.somemore.global.exception.ExceptionMessage.VOLUNTEER_APPLY_LIST_MISMATCH; +import com.somemore.facade.event.VolunteerReviewRequestEvent; +import com.somemore.global.common.event.ServerEventPublisher; +import com.somemore.global.common.event.ServerEventType; import com.somemore.global.exception.BadRequestException; +import com.somemore.notification.domain.NotificationSubType; import com.somemore.recruitboard.domain.RecruitBoard; import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; import com.somemore.volunteer.usecase.UpdateVolunteerUseCase; @@ -25,6 +29,7 @@ public class SettleVolunteerApplyFacadeService implements SettleVolunteerApplyFa private final VolunteerApplyQueryUseCase volunteerApplyQueryUseCase; private final RecruitBoardQueryUseCase recruitBoardQueryUseCase; private final UpdateVolunteerUseCase updateVolunteerUseCase; + private final ServerEventPublisher serverEventPublisher; @Override public void settleVolunteerApplies(VolunteerApplySettleRequestDto dto, UUID centerId) { @@ -41,6 +46,7 @@ public void settleVolunteerApplies(VolunteerApplySettleRequestDto dto, UUID cent applies.forEach(apply -> { apply.changeAttended(true); updateVolunteerUseCase.updateVolunteerStats(apply.getVolunteerId(), hours); + publishVolunteerReviewRequestEvent(apply, recruitBoard); }); } @@ -68,4 +74,17 @@ private void validateRecruitBoardConsistency(List applies, throw new BadRequestException(RECRUIT_BOARD_ID_MISMATCH); } } + + private void publishVolunteerReviewRequestEvent(VolunteerApply apply, RecruitBoard recruitBoard) { + VolunteerReviewRequestEvent event = VolunteerReviewRequestEvent.builder() + .type(ServerEventType.NOTIFICATION) + .subType(NotificationSubType.VOLUNTEER_REVIEW_REQUEST) + .volunteerId(apply.getVolunteerId()) + .volunteerApplyId(apply.getId()) + .centerId(recruitBoard.getCenterId()) + .recruitBoardId(recruitBoard.getId()) + .build(); + + serverEventPublisher.publish(event); + } } diff --git a/src/main/java/com/somemore/global/common/event/ServerEvent.java b/src/main/java/com/somemore/global/common/event/ServerEvent.java index 45da2293d..922b2832d 100644 --- a/src/main/java/com/somemore/global/common/event/ServerEvent.java +++ b/src/main/java/com/somemore/global/common/event/ServerEvent.java @@ -20,9 +20,9 @@ public abstract class ServerEvent> { private final LocalDateTime createdAt; protected ServerEvent( - @JsonProperty("type") ServerEventType type, - @JsonProperty("subType") T subType, - @JsonProperty("createdAt") LocalDateTime createdAt + @JsonProperty(value = "type", required = true) ServerEventType type, + @JsonProperty(value = "subType", required = true) T subType, + @JsonProperty(value = "createdAt", required = true) LocalDateTime createdAt ) { this.type = type; this.subType = subType; diff --git a/src/main/java/com/somemore/notification/converter/MessageConverter.java b/src/main/java/com/somemore/notification/converter/MessageConverter.java index 0dbe39005..60bb54c87 100644 --- a/src/main/java/com/somemore/notification/converter/MessageConverter.java +++ b/src/main/java/com/somemore/notification/converter/MessageConverter.java @@ -1,11 +1,14 @@ package com.somemore.notification.converter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.somemore.facade.event.VolunteerReviewRequestEvent; import com.somemore.notification.domain.Notification; import com.somemore.notification.domain.NotificationSubType; import com.somemore.volunteerapply.domain.ApplyStatus; -import com.somemore.volunteerapply.domain.VolunteerApplyStatusChangeEvent; +import com.somemore.volunteerapply.event.VolunteerApplyStatusChangeEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -24,7 +27,7 @@ public Notification from(String message) { return switch (NotificationSubType.from(eventType)) { case NOTE_BLAH_BLAH -> throw new UnsupportedOperationException("NOTE 알림 타입 처리 로직 미구현"); - case REVIEW_BLAH_BLAH -> throw new UnsupportedOperationException("REVIEW 알림 타입 처리 로직 미구현"); + case VOLUNTEER_REVIEW_REQUEST -> buildVolunteerReviewRequestNotification(message); case VOLUNTEER_APPLY_STATUS_CHANGE -> buildVolunteerApplyStatusChangeNotification(message); }; } catch (Exception e) { @@ -33,32 +36,40 @@ public Notification from(String message) { } } - private Notification buildVolunteerApplyStatusChangeNotification(String message) throws Exception { - VolunteerApplyStatusChangeEvent event = objectMapper.readValue(message, VolunteerApplyStatusChangeEvent.class); + private Notification buildVolunteerReviewRequestNotification(String message) throws JsonProcessingException { + VolunteerReviewRequestEvent event = objectMapper.readValue(message, VolunteerReviewRequestEvent.class); return Notification.builder() - .receiverId(event.getReceiverId()) - .title(buildNotificationTitle(event.getNewStatus())) - .type(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE) + .receiverId(event.getVolunteerId()) + .title(createVolunteerReviewRequestNotificationTitle()) + .type(NotificationSubType.VOLUNTEER_REVIEW_REQUEST) .relatedId(event.getRecruitBoardId()) .build(); } - private Notification handleNoteEvent(String message) { - // TODO: NOTE 이벤트를 처리하는 로직 구현 - throw new UnsupportedOperationException("NOTE 알림 타입 처리 로직 미구현"); + private Notification buildVolunteerApplyStatusChangeNotification(String message) throws JsonProcessingException { + VolunteerApplyStatusChangeEvent event = objectMapper.readValue(message, VolunteerApplyStatusChangeEvent.class); + + return Notification.builder() + .receiverId(event.getVolunteerId()) + .title(createVolunteerApplyStatusChangeNotificationTitle(event.getNewStatus())) + .type(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE) + .relatedId(event.getRecruitBoardId()) + .build(); } - private Notification handleReviewEvent(String message) { - // TODO: System 이벤트를 처리하는 로직 구현 - throw new UnsupportedOperationException("REVIEW 알림 타입 처리 로직 미구현"); + private String createVolunteerReviewRequestNotificationTitle() { + return "최근 활동하신 활동의 후기를 작성해 주세요!"; } - private String buildNotificationTitle(ApplyStatus newStatus) { + private String createVolunteerApplyStatusChangeNotificationTitle(ApplyStatus newStatus) { return switch (newStatus) { case APPROVED -> "봉사 활동 신청이 승인되었습니다."; case REJECTED -> "봉사 활동 신청이 거절되었습니다."; - default -> "봉사 활동 신청 상태가 변경되었습니다."; + default -> { + log.error("올바르지 않은 봉사 신청 상태입니다: {}", newStatus); + throw new IllegalArgumentException(); + } }; } } diff --git a/src/main/java/com/somemore/notification/domain/NotificationSubType.java b/src/main/java/com/somemore/notification/domain/NotificationSubType.java index ad0eb05d8..1313069d8 100644 --- a/src/main/java/com/somemore/notification/domain/NotificationSubType.java +++ b/src/main/java/com/somemore/notification/domain/NotificationSubType.java @@ -7,7 +7,7 @@ @RequiredArgsConstructor public enum NotificationSubType { NOTE_BLAH_BLAH("쪽지"), - REVIEW_BLAH_BLAH("후기 요청"), + VOLUNTEER_REVIEW_REQUEST("봉사 후기 요청"), VOLUNTEER_APPLY_STATUS_CHANGE("신청 상태 변경") ; diff --git a/src/main/java/com/somemore/notification/handler/NotificationHandlerImpl.java b/src/main/java/com/somemore/notification/handler/NotificationHandlerImpl.java index c193f59ce..d7254aa47 100644 --- a/src/main/java/com/somemore/notification/handler/NotificationHandlerImpl.java +++ b/src/main/java/com/somemore/notification/handler/NotificationHandlerImpl.java @@ -8,9 +8,11 @@ import com.somemore.sse.usecase.SseUseCase; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @Component @RequiredArgsConstructor +@Transactional public class NotificationHandlerImpl implements NotificationHandler { private final NotificationRepository notificationRepository; diff --git a/src/main/java/com/somemore/volunteerapply/domain/VolunteerApplyStatusChangeEvent.java b/src/main/java/com/somemore/volunteerapply/event/VolunteerApplyStatusChangeEvent.java similarity index 61% rename from src/main/java/com/somemore/volunteerapply/domain/VolunteerApplyStatusChangeEvent.java rename to src/main/java/com/somemore/volunteerapply/event/VolunteerApplyStatusChangeEvent.java index 034121192..bbfc62569 100644 --- a/src/main/java/com/somemore/volunteerapply/domain/VolunteerApplyStatusChangeEvent.java +++ b/src/main/java/com/somemore/volunteerapply/event/VolunteerApplyStatusChangeEvent.java @@ -1,10 +1,11 @@ -package com.somemore.volunteerapply.domain; +package com.somemore.volunteerapply.event; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.somemore.global.common.event.ServerEvent; import com.somemore.global.common.event.ServerEventType; import com.somemore.notification.domain.NotificationSubType; +import com.somemore.volunteerapply.domain.ApplyStatus; import lombok.Getter; import lombok.experimental.SuperBuilder; @@ -15,7 +16,7 @@ @SuperBuilder public class VolunteerApplyStatusChangeEvent extends ServerEvent { - private final UUID receiverId; + private final UUID volunteerId; private final Long volunteerApplyId; private final UUID centerId; private final Long recruitBoardId; @@ -24,15 +25,15 @@ public class VolunteerApplyStatusChangeEvent extends ServerEvent messageConverter.from(invalidMessage)); + } + + @Test + @DisplayName("필수 필드가 누락된 메시지를 변환하면 IllegalStateException을 던진다") + void testMissingFields() { + // given + String messageWithMissingFields = """ + { + "type": "NOTIFICATION", + "subType": "VOLUNTEER_REVIEW_REQUEST" + } + """; + + // when & then + assertThrows(IllegalStateException.class, () -> messageConverter.from(messageWithMissingFields)); + } +} \ No newline at end of file diff --git a/src/test/java/com/somemore/notification/handler/NotificationHandlerTest.java b/src/test/java/com/somemore/notification/handler/NotificationHandlerTest.java index 0f1546713..80c07db63 100644 --- a/src/test/java/com/somemore/notification/handler/NotificationHandlerTest.java +++ b/src/test/java/com/somemore/notification/handler/NotificationHandlerTest.java @@ -35,10 +35,11 @@ void handle() { { "type": "NOTIFICATION", "subType": "VOLUNTEER_APPLY_STATUS_CHANGE", - "receiverId": "123e4567-e89b-12d3-a456-426614174000", + "volunteerId": "123e4567-e89b-12d3-a456-426614174000", "volunteerApplyId": 123, "centerId": "123e4567-e89b-12d3-a456-426614174001", "recruitBoardId": 456, + "oldStatus": "WAITING", "newStatus": "APPROVED", "createdAt": "2024-12-05T10:00:00" } @@ -61,5 +62,7 @@ void handle() { assertThat(savedNotification.getRelatedId()).isEqualTo(456L); // 프론트 요구사항: 123L(봉사신청아이디), 456L(모집글아이디) assertThat(savedNotification.isRead()).isFalse(); assertThat(savedNotification.getCreatedAt()).isEqualTo(notification.getCreatedAt()); + assertThat(savedNotification.getCreatedAt()).isNotNull(); } + } diff --git a/src/test/java/com/somemore/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/somemore/notification/service/NotificationCommandServiceTest.java index 30231934e..1a5cf832b 100644 --- a/src/test/java/com/somemore/notification/service/NotificationCommandServiceTest.java +++ b/src/test/java/com/somemore/notification/service/NotificationCommandServiceTest.java @@ -126,7 +126,7 @@ private Notification createNotification(UUID receiverId) { return Notification.builder() .receiverId(receiverId) .title("Unread") - .type(NotificationSubType.REVIEW_BLAH_BLAH) + .type(NotificationSubType.VOLUNTEER_REVIEW_REQUEST) .relatedId(1L) .build(); } diff --git a/src/test/java/com/somemore/notification/service/NotificationQueryServiceTest.java b/src/test/java/com/somemore/notification/service/NotificationQueryServiceTest.java index 5ac0b661f..2b3522599 100644 --- a/src/test/java/com/somemore/notification/service/NotificationQueryServiceTest.java +++ b/src/test/java/com/somemore/notification/service/NotificationQueryServiceTest.java @@ -55,7 +55,7 @@ void getReadNotifications() { Notification readNotification = Notification.builder() .title("Read Notification") - .type(NotificationSubType.REVIEW_BLAH_BLAH) + .type(NotificationSubType.VOLUNTEER_REVIEW_REQUEST) .receiverId(receiverId) .relatedId(2L) .build();