-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/287 봉사 기록 엔티티와 이벤트 기반 생성 방식 구현 #293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fbcd8ac
18a9cef
2c28207
fce9791
e9a61b0
e022479
2c6d162
7ff6c7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. serverEventPublisher를 활용해서 추상화하는 것이 좋을 것 같습니다. 혹시 봉사 기록 이벤트만을 위한 퍼블리셔를 만들고 의존하신 특별한 이유가 있나요?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹은 봉사 기록 이벤트 내부에서 정적 팩토리 메서드를 활용할 수 있을 것 같습니다.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 처음에 이벤트에 대한 개념이 확실히 잡히지 않아서 현재는 봉사 시간 정산 이벤트가 발생하고 이를 구독하는 클래스에서 자신들이 필요한 로직을 수행하도록 봉사 시간 정산 이벤트가 발생하면 RedisSettleVolunteerHoursSubscriber 에서 이를 구독하여
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이거 방식이 달라서 상의해서 한가지로 통합하는게 좋아보입니다.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @7zrv 좋습니다. 봉사 시간 정산 이벤트가 발생하면 그것을 봉사 시간 정산 이벤트 구독자가 받고 그제야 봉사 시간 정산 이벤트로부터 발생해야 하는 리뷰 요청 알림 이벤트와 봉사 기록 생성 이벤트로 나누어져서 두 번의 이벤트를 발생시키면 좋을 것 같습니다. 두 번의 이벤트가 발생하는 시점에서 범수 님이 말해주신 것 중에서 골라야 하는데, 제 생각에는 이벤트를 발생시킨다는 개념은 serverEventPublisher에게 맡기고 이벤트는 이벤트 클래스의 정적 메서드를 활용해서 생성하는 게 좋을 것 같다고 생각했습니다. -> publishVolunteerReviewRequestEvent(apply, recruitBoard) 메서드를 사용하자. 하지만 빌더 패턴을 드러내지 말자 (이벤트 클래스의 정적 팩토리 메서드를 활용하자) 라고 생각했습니다! |
||
| }); | ||
|
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<DomainEventSubType> { | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋네요~ |
||
| 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() | ||
| ); | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<VolunteerRecord, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.somemore.domains.volunteerrecord.repository; | ||
|
|
||
| import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; | ||
|
|
||
| public interface VolunteerRecordRepository { | ||
| void save(VolunteerRecord volunteerRecord); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 서비스 클래스들은 CreateDomainService 이런식으로 지었는데 여기만 DomainCreateService로 지은 이유가 있으신가요? |
||
| private final VolunteerRecordRepository volunteerRecordRepository; | ||
|
|
||
| public void create(VolunteerRecord volunteerRecord) { | ||
| volunteerRecordRepository.save(volunteerRecord); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.somemore.domains.volunteerrecord.usecase; | ||
|
|
||
| import com.somemore.domains.volunteerrecord.domain.VolunteerRecord; | ||
|
|
||
| public interface VolunteerRecordCreateUseCase { | ||
| void create(VolunteerRecord volunteerRecord); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
조치해주셔서 감사합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기 null인 이유는 추후 구현인건가요?
로직을 몰라서 여쭤봅니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 구조 자체가 조금 잘못돼서 추후에 수정해야하는 부분입니다.