Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<NotificationSubType> {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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);
});

}
Expand Down Expand Up @@ -68,4 +74,17 @@ private void validateRecruitBoardConsistency(List<VolunteerApply> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public abstract class ServerEvent<T extends Enum<T>> {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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) {
Expand All @@ -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();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@RequiredArgsConstructor
public enum NotificationSubType {
NOTE_BLAH_BLAH("쪽지"),
REVIEW_BLAH_BLAH("후기 요청"),
VOLUNTEER_REVIEW_REQUEST("봉사 후기 요청"),
VOLUNTEER_APPLY_STATUS_CHANGE("신청 상태 변경")
;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -15,7 +16,7 @@
@SuperBuilder
public class VolunteerApplyStatusChangeEvent extends ServerEvent<NotificationSubType> {

private final UUID receiverId;
private final UUID volunteerId;
private final Long volunteerApplyId;
private final UUID centerId;
private final Long recruitBoardId;
Expand All @@ -24,15 +25,15 @@ public class VolunteerApplyStatusChangeEvent extends ServerEvent<NotificationSub

@JsonCreator
public VolunteerApplyStatusChangeEvent(
@JsonProperty("receiverId") UUID receiverId,
@JsonProperty("volunteerApplyId") Long volunteerApplyId,
@JsonProperty("centerId") UUID centerId,
@JsonProperty("recruitBoardId") Long recruitBoardId,
@JsonProperty("oldStatus") ApplyStatus oldStatus,
@JsonProperty("newStatus") ApplyStatus newStatus
@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,
@JsonProperty(value = "oldStatus", required = true) ApplyStatus oldStatus,
@JsonProperty(value = "newStatus", required = true) ApplyStatus newStatus
) {
super(ServerEventType.NOTIFICATION, NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE, LocalDateTime.now());
this.receiverId = receiverId;
this.volunteerId = volunteerId;
this.volunteerApplyId = volunteerApplyId;
this.centerId = centerId;
this.recruitBoardId = recruitBoardId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import com.somemore.notification.domain.NotificationSubType;
import com.somemore.recruitboard.domain.RecruitBoard;
import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase;
import com.somemore.volunteerapply.domain.ApplyStatus;
import com.somemore.volunteerapply.domain.VolunteerApply;
import com.somemore.volunteerapply.domain.VolunteerApplyStatusChangeEvent;
import com.somemore.volunteerapply.event.VolunteerApplyStatusChangeEvent;
import com.somemore.volunteerapply.repository.VolunteerApplyRepository;
import com.somemore.volunteerapply.usecase.ApproveVolunteerApplyUseCase;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -38,10 +39,11 @@ public void approve(Long id, UUID centerId) {
validateWriter(recruitBoard, centerId);
validateBoardStatus(recruitBoard);

ApplyStatus oldStatus = apply.getStatus();
apply.changeStatus(APPROVED);
volunteerApplyRepository.save(apply);

publishVolunteerApplyStatusChangeEvent(apply.getVolunteerId(), id, recruitBoard, apply);
publishVolunteerApplyStatusChangeEvent(apply, recruitBoard, oldStatus);
}

private VolunteerApply getVolunteerApply(Long id) {
Expand All @@ -63,14 +65,15 @@ private void validateBoardStatus(RecruitBoard recruitBoard) {
}
}

private void publishVolunteerApplyStatusChangeEvent(UUID receiverId, Long id, RecruitBoard recruitBoard, VolunteerApply apply) {
private void publishVolunteerApplyStatusChangeEvent(VolunteerApply apply, RecruitBoard recruitBoard, ApplyStatus oldStatus) {
VolunteerApplyStatusChangeEvent event = VolunteerApplyStatusChangeEvent.builder()
.type(ServerEventType.NOTIFICATION)
.subType(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE)
.receiverId(receiverId)
.volunteerApplyId(id)
.volunteerId(apply.getVolunteerId())
.volunteerApplyId(apply.getId())
.centerId(recruitBoard.getCenterId())
.recruitBoardId(recruitBoard.getId())
.oldStatus(oldStatus)
.newStatus(apply.getStatus())
.build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.somemore.notification.converter;

import com.somemore.IntegrationTestSupport;
import com.somemore.notification.domain.Notification;
import com.somemore.notification.domain.NotificationSubType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class MessageConverterTest extends IntegrationTestSupport {

@Autowired
private MessageConverter messageConverter;

@Test
@DisplayName("VOLUNTEER_REVIEW_REQUEST 메시지를 변환하면 Notification 객체를 반환한다")
void testVolunteerReviewRequestConversion() {
// given
String message = """
{
"type": "NOTIFICATION",
"subType": "VOLUNTEER_REVIEW_REQUEST",
"volunteerId": "123e4567-e89b-12d3-a456-426614174000",
"volunteerApplyId": "1",
"centerId": "123e4567-e89b-12d3-a456-426614174001",
"recruitBoardId": 456,
"createdAt": "2024-12-05T10:00:00"
}
""";

// when
Notification notification = messageConverter.from(message);

// then
assertThat(notification.getReceiverId()).isEqualTo(UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
assertThat(notification.getTitle()).isEqualTo("최근 활동하신 활동의 후기를 작성해 주세요!");
assertThat(notification.getType()).isEqualTo(NotificationSubType.VOLUNTEER_REVIEW_REQUEST);
assertThat(notification.getRelatedId()).isEqualTo(456L);
}

@Test
@DisplayName("임의의 필드가 추가된 VOLUNTEER_APPLY_STATUS_CHANGE 메시지를 변환해도 Notification 객체를 반환한다")
void testVolunteerApplyStatusChangeConversion() {
// given
String message = """
{
"extraField": "this should be ignored",
"extraField": "this should be ignored",
"extraField": "this should be ignored",
"extraField": "this should be ignored",
"extraField": "this should be ignored",
"type": "NOTIFICATION",
"subType": "VOLUNTEER_APPLY_STATUS_CHANGE",
"volunteerId": "123e4567-e89b-12d3-a456-426614174000",
"centerId": "123e4567-e89b-12d3-a456-426614174001",
"volunteerApplyId": "1",
"recruitBoardId": 456,
"oldStatus": "WAITING",
"newStatus": "APPROVED",
"createdAt": "2024-12-05T10:00:00"
}
""";

// when
Notification notification = messageConverter.from(message);

// then
assertThat(notification.getReceiverId()).isEqualTo(UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
assertThat(notification.getTitle()).isEqualTo("봉사 활동 신청이 승인되었습니다.");
assertThat(notification.getType()).isEqualTo(NotificationSubType.VOLUNTEER_APPLY_STATUS_CHANGE);
assertThat(notification.getRelatedId()).isEqualTo(456L);
}

@Test
@DisplayName("잘못된 JSON 메시지를 변환하면 IllegalStateException을 던진다")
void testInvalidJson() {
// given
String invalidMessage = "{ invalid-json }";

// when
// then
assertThrows(IllegalStateException.class, () -> 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));
}
}
Loading
Loading