diff --git a/src/main/java/com/somemore/domains/interestcenter/event/converter/CreateRecruitBoardMessageConverter.java b/src/main/java/com/somemore/domains/interestcenter/event/converter/CreateRecruitBoardMessageConverter.java index ccfd840db..b7022c62e 100644 --- a/src/main/java/com/somemore/domains/interestcenter/event/converter/CreateRecruitBoardMessageConverter.java +++ b/src/main/java/com/somemore/domains/interestcenter/event/converter/CreateRecruitBoardMessageConverter.java @@ -23,6 +23,7 @@ public CreateRecruitBoardEvent from(String message) { return switch (DomainEventSubType.from(eventType)) { case CREATE_RECRUIT_BOARD -> parseCreateRecruitBoardEvent(message); + case VOLUNTEER_HOURS_SETTLE -> null; }; } catch (Exception e) { log.error(e.getMessage()); @@ -34,4 +35,5 @@ private CreateRecruitBoardEvent parseCreateRecruitBoardEvent(String message) thr return objectMapper.readValue(message, CreateRecruitBoardEvent.class); } + } diff --git a/src/main/java/com/somemore/domains/volunteerapply/service/SettleVolunteerApplyFacadeService.java b/src/main/java/com/somemore/domains/volunteerapply/service/SettleVolunteerApplyFacadeService.java index 66158e59d..93a213fd6 100644 --- a/src/main/java/com/somemore/domains/volunteerapply/service/SettleVolunteerApplyFacadeService.java +++ b/src/main/java/com/somemore/domains/volunteerapply/service/SettleVolunteerApplyFacadeService.java @@ -9,6 +9,7 @@ import com.somemore.domains.volunteerapply.event.VolunteerReviewRequestEvent; import com.somemore.domains.volunteerapply.usecase.SettleVolunteerApplyFacadeUseCase; import com.somemore.domains.volunteerapply.usecase.VolunteerApplyQueryUseCase; +import com.somemore.domains.volunteerrecord.event.VolunteerRecordEventPublisher; import com.somemore.global.common.event.ServerEventPublisher; import com.somemore.global.common.event.ServerEventType; import com.somemore.global.exception.BadRequestException; @@ -30,6 +31,7 @@ public class SettleVolunteerApplyFacadeService implements SettleVolunteerApplyFa private final RecruitBoardQueryUseCase recruitBoardQueryUseCase; private final UpdateVolunteerUseCase updateVolunteerUseCase; private final ServerEventPublisher serverEventPublisher; + private final VolunteerRecordEventPublisher volunteerRecordEventPublisher; @Override public void settleVolunteerApplies(VolunteerApplySettleRequestDto dto, UUID centerId) { @@ -47,6 +49,7 @@ public void settleVolunteerApplies(VolunteerApplySettleRequestDto dto, UUID cent apply.changeAttended(true); updateVolunteerUseCase.updateVolunteerStats(apply.getVolunteerId(), hours); publishVolunteerReviewRequestEvent(apply, recruitBoard); + volunteerRecordEventPublisher.publishVolunteerRecordCreateEvent(apply, recruitBoard); }); } diff --git a/src/main/java/com/somemore/domains/volunteerrecord/domain/VolunteerRecord.java b/src/main/java/com/somemore/domains/volunteerrecord/domain/VolunteerRecord.java new file mode 100644 index 000000000..6174bb91d --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/domain/VolunteerRecord.java @@ -0,0 +1,54 @@ +package com.somemore.domains.volunteerrecord.domain; + +import com.somemore.global.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.UUID; + +import static jakarta.persistence.GenerationType.IDENTITY; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "volunteer_record") +public class VolunteerRecord extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(name = "volunteer_id", nullable = false) + private UUID volunteerId; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "volunteer_date", nullable = false) + private LocalDate volunteerDate; + + @Column(name = "volunteer_hours", nullable = false) + private int volunteerHours; + + @Builder + private VolunteerRecord(UUID volunteerId, String title, LocalDate volunteerDate, int volunteerHours) { + this.volunteerId = volunteerId; + this.title = title; + this.volunteerDate = volunteerDate; + this.volunteerHours = volunteerHours; + } + + public static VolunteerRecord create(UUID volunteerId, String title, LocalDate volunteerDate, int volunteerHours) { + return VolunteerRecord.builder() + .volunteerId(volunteerId) + .title(title) + .volunteerDate(volunteerDate) + .volunteerHours(volunteerHours) + .build(); + } + +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/event/VolunteerRecordCreateEvent.java b/src/main/java/com/somemore/domains/volunteerrecord/event/VolunteerRecordCreateEvent.java new file mode 100644 index 000000000..56cbda80f --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/event/VolunteerRecordCreateEvent.java @@ -0,0 +1,39 @@ +package com.somemore.domains.volunteerrecord.event; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.somemore.global.common.event.DomainEventSubType; +import com.somemore.global.common.event.ServerEvent; +import com.somemore.global.common.event.ServerEventType; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@SuperBuilder +public class VolunteerRecordCreateEvent extends ServerEvent { + + private final UUID volunteerId; + private final String title; + private final LocalDate volunteerDate; + private final int volunteerHours; + + @JsonCreator + private VolunteerRecordCreateEvent( + @JsonProperty(value = "volunteerId", required = true) UUID volunteerId, + @JsonProperty(value = "title", required = true) String title, + @JsonProperty(value = "volunteerDate", required = true) LocalDate volunteerDate, + @JsonProperty(value = "volunteerHours", required = true) int volunteerHours) { + + super(ServerEventType.DOMAIN_EVENT, DomainEventSubType.VOLUNTEER_HOURS_SETTLE, LocalDateTime.now()); + + this.volunteerId = volunteerId; + this.title = title; + this.volunteerDate = volunteerDate; + this.volunteerHours = volunteerHours; + } + +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/event/VolunteerRecordEventPublisher.java b/src/main/java/com/somemore/domains/volunteerrecord/event/VolunteerRecordEventPublisher.java new file mode 100644 index 000000000..373bcb7b9 --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/event/VolunteerRecordEventPublisher.java @@ -0,0 +1,31 @@ +package com.somemore.domains.volunteerrecord.event; + +import com.somemore.domains.recruitboard.domain.RecruitBoard; +import com.somemore.domains.volunteerapply.domain.VolunteerApply; +import com.somemore.global.common.event.DomainEventSubType; +import com.somemore.global.common.event.ServerEventPublisher; +import com.somemore.global.common.event.ServerEventType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class VolunteerRecordEventPublisher { + + private final ServerEventPublisher serverEventPublisher; + + public void publishVolunteerRecordCreateEvent(VolunteerApply apply, RecruitBoard recruitBoard) { + + VolunteerRecordCreateEvent event = VolunteerRecordCreateEvent + .builder() + .type(ServerEventType.DOMAIN_EVENT) + .subType(DomainEventSubType.VOLUNTEER_HOURS_SETTLE) + .volunteerId(apply.getVolunteerId()) + .title(recruitBoard.getTitle()) + .volunteerDate(recruitBoard.getRecruitmentInfo().getVolunteerEndDateTime().toLocalDate()) + .volunteerHours(recruitBoard.getRecruitmentInfo().getVolunteerHours()) + .build(); + + serverEventPublisher.publish(event); + } +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/event/convert/VolunteerRecordMessageConverter.java b/src/main/java/com/somemore/domains/volunteerrecord/event/convert/VolunteerRecordMessageConverter.java new file mode 100644 index 000000000..1f86b5eb6 --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/event/convert/VolunteerRecordMessageConverter.java @@ -0,0 +1,51 @@ +package com.somemore.domains.volunteerrecord.event.convert; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; +import com.somemore.domains.volunteerrecord.event.VolunteerRecordCreateEvent; +import com.somemore.global.common.event.DomainEventSubType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class VolunteerRecordMessageConverter { + + private static final String SUB_TYPE = "subType"; + private final ObjectMapper objectMapper; + + public VolunteerRecord from(String message) { + try { + JsonNode rootNode = objectMapper.readTree(message); + String eventType = rootNode.get(SUB_TYPE).asText(); + + return switch (DomainEventSubType.from(eventType)) { + case VOLUNTEER_HOURS_SETTLE -> convertToVolunteerRecord(message); + default -> { + log.error("지원하지 않는 이벤트 타입입니다: {}", eventType); + throw new IllegalArgumentException("지원하지 않는 이벤트 타입입니다: " + eventType); + } + }; + } catch (Exception e) { + log.error("메시지 변환 실패: {}", e.getMessage()); + throw new IllegalStateException("메시지 변환 중 오류가 발생했습니다.", e); + } + } + + private VolunteerRecord convertToVolunteerRecord(String message) throws JsonProcessingException { + + VolunteerRecordCreateEvent volunteerRecordCreateEvent = objectMapper.readValue(message, VolunteerRecordCreateEvent.class); + + return VolunteerRecord.create( + volunteerRecordCreateEvent.getVolunteerId(), + volunteerRecordCreateEvent.getTitle(), + volunteerRecordCreateEvent.getVolunteerDate(), + volunteerRecordCreateEvent.getVolunteerHours() + ); + + } +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/event/handler/SettleVolunteerHoursHandler.java b/src/main/java/com/somemore/domains/volunteerrecord/event/handler/SettleVolunteerHoursHandler.java new file mode 100644 index 000000000..699bc2954 --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/event/handler/SettleVolunteerHoursHandler.java @@ -0,0 +1,9 @@ +package com.somemore.domains.volunteerrecord.event.handler; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; + +public interface SettleVolunteerHoursHandler { + + void handle(VolunteerRecord volunteerRecord); + +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/event/handler/SettleVolunteerHoursHandlerImpl.java b/src/main/java/com/somemore/domains/volunteerrecord/event/handler/SettleVolunteerHoursHandlerImpl.java new file mode 100644 index 000000000..4336ea36c --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/event/handler/SettleVolunteerHoursHandlerImpl.java @@ -0,0 +1,21 @@ +package com.somemore.domains.volunteerrecord.event.handler; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; +import com.somemore.domains.volunteerrecord.usecase.VolunteerRecordCreateUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +@Transactional +public class SettleVolunteerHoursHandlerImpl implements SettleVolunteerHoursHandler { + + private final VolunteerRecordCreateUseCase volunteerRecordCreateUseCase; + + @Override + public void handle(VolunteerRecord volunteerRecord) { + + volunteerRecordCreateUseCase.create(volunteerRecord); + } +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/event/subscriber/RedisSettleVolunteerHoursSubscriber.java b/src/main/java/com/somemore/domains/volunteerrecord/event/subscriber/RedisSettleVolunteerHoursSubscriber.java new file mode 100644 index 000000000..dc0858822 --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/event/subscriber/RedisSettleVolunteerHoursSubscriber.java @@ -0,0 +1,27 @@ +package com.somemore.domains.volunteerrecord.event.subscriber; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; +import com.somemore.domains.volunteerrecord.event.convert.VolunteerRecordMessageConverter; +import com.somemore.domains.volunteerrecord.event.handler.SettleVolunteerHoursHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RedisSettleVolunteerHoursSubscriber implements MessageListener { + + private final SettleVolunteerHoursHandler settleVolunteerHoursHandler; + private final VolunteerRecordMessageConverter messageConverter; + + @Override + public void onMessage(Message message, byte[] pattern) { + + VolunteerRecord volunteerRecord = messageConverter.from( + new String(message.getBody()) + ); + + settleVolunteerHoursHandler.handle(volunteerRecord); + } +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordJpaRepository.java b/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordJpaRepository.java new file mode 100644 index 000000000..5e2562be8 --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordJpaRepository.java @@ -0,0 +1,7 @@ +package com.somemore.domains.volunteerrecord.repository; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VolunteerRecordJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordRepository.java b/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordRepository.java new file mode 100644 index 000000000..e50a0125d --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordRepository.java @@ -0,0 +1,7 @@ +package com.somemore.domains.volunteerrecord.repository; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; + +public interface VolunteerRecordRepository { + void save(VolunteerRecord volunteerRecord); +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordRepositoryImpl.java b/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordRepositoryImpl.java new file mode 100644 index 000000000..b5d46fd3d --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/repository/VolunteerRecordRepositoryImpl.java @@ -0,0 +1,20 @@ +package com.somemore.domains.volunteerrecord.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class VolunteerRecordRepositoryImpl implements VolunteerRecordRepository { + + private final JPAQueryFactory queryFactory; + private final VolunteerRecordJpaRepository volunteerRecordJpaRepository; + + @Override + public void save(VolunteerRecord volunteerRecord) { + volunteerRecordJpaRepository.save(volunteerRecord); + } + +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/service/VolunteerRecordCreateService.java b/src/main/java/com/somemore/domains/volunteerrecord/service/VolunteerRecordCreateService.java new file mode 100644 index 000000000..22ad6cb82 --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/service/VolunteerRecordCreateService.java @@ -0,0 +1,20 @@ +package com.somemore.domains.volunteerrecord.service; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; +import com.somemore.domains.volunteerrecord.repository.VolunteerRecordRepository; +import com.somemore.domains.volunteerrecord.usecase.VolunteerRecordCreateUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@Transactional +public class VolunteerRecordCreateService implements VolunteerRecordCreateUseCase { + + private final VolunteerRecordRepository volunteerRecordRepository; + + public void create(VolunteerRecord volunteerRecord) { + volunteerRecordRepository.save(volunteerRecord); + } +} diff --git a/src/main/java/com/somemore/domains/volunteerrecord/usecase/VolunteerRecordCreateUseCase.java b/src/main/java/com/somemore/domains/volunteerrecord/usecase/VolunteerRecordCreateUseCase.java new file mode 100644 index 000000000..6c9c2725a --- /dev/null +++ b/src/main/java/com/somemore/domains/volunteerrecord/usecase/VolunteerRecordCreateUseCase.java @@ -0,0 +1,7 @@ +package com.somemore.domains.volunteerrecord.usecase; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; + +public interface VolunteerRecordCreateUseCase { + void create(VolunteerRecord volunteerRecord); +} diff --git a/src/main/java/com/somemore/global/common/event/DomainEventSubType.java b/src/main/java/com/somemore/global/common/event/DomainEventSubType.java index c23821960..c16328807 100644 --- a/src/main/java/com/somemore/global/common/event/DomainEventSubType.java +++ b/src/main/java/com/somemore/global/common/event/DomainEventSubType.java @@ -7,6 +7,7 @@ @RequiredArgsConstructor public enum DomainEventSubType { CREATE_RECRUIT_BOARD("모집 글 등록"), + VOLUNTEER_HOURS_SETTLE("봉사 시간 정산") ; private final String description; diff --git a/src/main/java/com/somemore/global/redis/registrar/RedisListenerRegistrar.java b/src/main/java/com/somemore/global/redis/registrar/RedisListenerRegistrar.java index 196b96c17..e0fd5e4d1 100644 --- a/src/main/java/com/somemore/global/redis/registrar/RedisListenerRegistrar.java +++ b/src/main/java/com/somemore/global/redis/registrar/RedisListenerRegistrar.java @@ -2,6 +2,7 @@ import com.somemore.domains.interestcenter.event.subscriber.RedisCreateRecruitBoardSubscriber; import com.somemore.domains.notification.event.subscriber.RedisNotificationSubscriber; +import com.somemore.domains.volunteerrecord.event.subscriber.RedisSettleVolunteerHoursSubscriber; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,6 +18,7 @@ public class RedisListenerRegistrar { private final RedisMessageListenerContainer container; private final RedisNotificationSubscriber redisNotificationSubscriber; private final RedisCreateRecruitBoardSubscriber redisCreateRecruitBoardSubscriber; + private final RedisSettleVolunteerHoursSubscriber redisSettleVolunteerHoursSubscriber; private final ChannelTopic notificationTopic; private final ChannelTopic domainEventTopic; @@ -25,9 +27,11 @@ public void registerListeners() { registerNotificationListener(); } + // 알림 리스너와 분리 private void registerNotificationListener() { container.addMessageListener(redisNotificationSubscriber, notificationTopic); container.addMessageListener(redisCreateRecruitBoardSubscriber, domainEventTopic); + container.addMessageListener(redisSettleVolunteerHoursSubscriber, domainEventTopic); log.info("리스너가 토픽에 성공적으로 등록되었습니다."); } } diff --git a/src/test/java/com/somemore/domains/volunteerrecord/domain/VolunteerRecordTest.java b/src/test/java/com/somemore/domains/volunteerrecord/domain/VolunteerRecordTest.java new file mode 100644 index 000000000..c7eb2048d --- /dev/null +++ b/src/test/java/com/somemore/domains/volunteerrecord/domain/VolunteerRecordTest.java @@ -0,0 +1,30 @@ +package com.somemore.domains.volunteerrecord.domain; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class VolunteerRecordTest { + + @Test + void createVolunteerRecord_success() { + // Given + UUID volunteerId = UUID.randomUUID(); + String title = "서울 도서관 봉사"; + LocalDate volunteerDate = LocalDate.of(2025, 1, 8); + int volunteerHours = 4; + + // When + VolunteerRecord volunteerRecord = VolunteerRecord.create(volunteerId, title, volunteerDate, volunteerHours); + + // Then + assertThat(volunteerRecord).isNotNull(); + assertThat(volunteerRecord.getVolunteerId()).isEqualTo(volunteerId); + assertThat(volunteerRecord.getTitle()).isEqualTo(title); + assertThat(volunteerRecord.getVolunteerDate()).isEqualTo(volunteerDate); + assertThat(volunteerRecord.getVolunteerHours()).isEqualTo(volunteerHours); + } +} diff --git a/src/test/java/com/somemore/domains/volunteerrecord/service/VolunteerRecordCreateServiceTest.java b/src/test/java/com/somemore/domains/volunteerrecord/service/VolunteerRecordCreateServiceTest.java new file mode 100644 index 000000000..e45431c0b --- /dev/null +++ b/src/test/java/com/somemore/domains/volunteerrecord/service/VolunteerRecordCreateServiceTest.java @@ -0,0 +1,49 @@ +package com.somemore.domains.volunteerrecord.service; + +import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; +import com.somemore.domains.volunteerrecord.repository.VolunteerRecordJpaRepository; +import com.somemore.support.IntegrationTestSupport; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class VolunteerRecordCreateServiceTest extends IntegrationTestSupport { + + @Autowired + private VolunteerRecordCreateService volunteerRecordCreateService; + + @Autowired + private VolunteerRecordJpaRepository volunteerRecordJpaRepository; + + @DisplayName("봉사 기록을 저장할 수 있다.") + @Test + void createVolunteerRecord() { + //given + UUID volunteerId = UUID.randomUUID(); + VolunteerRecord volunteerRecord = VolunteerRecord.create( + volunteerId, + "서울 도서관 봉사", + LocalDate.now(), + 4 + ); + + //when + volunteerRecordCreateService.create(volunteerRecord); + + //then + VolunteerRecord savedRecord = volunteerRecordJpaRepository.findById(volunteerRecord.getId()) + .orElseThrow(() -> new AssertionError("저장되지 않은 데이터")); + + assertThat(savedRecord).isNotNull(); + assertThat(savedRecord.getVolunteerId()).isEqualTo(volunteerId); + assertThat(savedRecord.getTitle()).isEqualTo("서울 도서관 봉사"); + assertThat(savedRecord.getVolunteerDate()).isEqualTo(volunteerRecord.getVolunteerDate()); + assertThat(savedRecord.getVolunteerHours()).isEqualTo(4); + } + +}