diff --git a/core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplicationCanceled.java b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplicationCanceled.java new file mode 100644 index 00000000..a2c9f915 --- /dev/null +++ b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplicationCanceled.java @@ -0,0 +1,4 @@ +package kr.ac.kookmin.stream.common.event; + +/** 회원이 행사 신청을 취소했다. 행사명은 알림 문구에 쓰려고 취소 시점의 값을 싣는다. */ +public record EventApplicationCanceled(Long memberId, Long eventId, String eventTitle) implements DomainEvent {} diff --git a/core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplied.java b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplied.java new file mode 100644 index 00000000..e01635c7 --- /dev/null +++ b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplied.java @@ -0,0 +1,4 @@ +package kr.ac.kookmin.stream.common.event; + +/** 회원이 행사를 신청했다. 행사명은 알림 문구에 쓰려고 신청 시점의 값을 싣는다. */ +public record EventApplied(Long memberId, Long eventId, String eventTitle) implements DomainEvent {} diff --git a/core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerAssigned.java b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerAssigned.java new file mode 100644 index 00000000..a264bc65 --- /dev/null +++ b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerAssigned.java @@ -0,0 +1,4 @@ +package kr.ac.kookmin.stream.common.event; + +/** 회원이 사물함을 신청해 배정받았다. 신청과 배정이 동시에 일어난다. */ +public record LockerAssigned(Long memberId, Long lockerPeriodId) implements DomainEvent {} diff --git a/core/common/src/main/java/kr/ac/kookmin/stream/common/event/NoticeCreated.java b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/NoticeCreated.java new file mode 100644 index 00000000..aea81c71 --- /dev/null +++ b/core/common/src/main/java/kr/ac/kookmin/stream/common/event/NoticeCreated.java @@ -0,0 +1,4 @@ +package kr.ac.kookmin.stream.common.event; + +/** 운영진이 새 공지를 등록했다. 공지는 저장하는 즉시 공개된다. */ +public record NoticeCreated(Long noticeId) implements DomainEvent {} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventApplicationServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventApplicationServiceImpl.java index f77b88e0..6598dfd9 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventApplicationServiceImpl.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventApplicationServiceImpl.java @@ -7,6 +7,9 @@ import java.util.stream.Collectors; import kr.ac.kookmin.stream.common.BusinessException; import kr.ac.kookmin.stream.common.CursorSliceResult; +import kr.ac.kookmin.stream.common.OutboxWriter; +import kr.ac.kookmin.stream.common.event.EventApplicationCanceled; +import kr.ac.kookmin.stream.common.event.EventApplied; import kr.ac.kookmin.stream.event.domain.event.domain.Event; import kr.ac.kookmin.stream.event.domain.event.domain.EventApplication; import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationAnswer; @@ -31,6 +34,7 @@ class EventApplicationServiceImpl implements EventApplicationService { private final EventRepository eventRepository; private final EventApplyAnswerValidator eventApplyAnswerValidator; + private final OutboxWriter outboxWriter; @Override @Transactional(readOnly = true) @@ -55,6 +59,7 @@ public EventApplicationResult apply(Long eventId, Long memberId, EventApplyComma EventApplication.create(eventId, memberId, LocalDateTime.now()) ); eventRepository.saveAnswers(toAnswers(application.getId(), command)); + outboxWriter.record(new EventApplied(memberId, eventId, event.getTitle())); return new EventApplicationResult(application.getId(), event); } @@ -119,6 +124,7 @@ public void cancelApplication(Long applicationId, Long memberId) { application.cancel(now); eventRepository.saveApplication(application); + outboxWriter.record(new EventApplicationCanceled(memberId, event.getId(), event.getTitle())); } /** 남의 신청은 없는 것으로 보여야 하므로 조회 단계에서 회원 식별자로 함께 거른다. */ diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImpl.java index 1f3f3c0c..f766c670 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImpl.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImpl.java @@ -6,6 +6,8 @@ import java.util.function.Function; import java.util.stream.Collectors; import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.common.OutboxWriter; +import kr.ac.kookmin.stream.common.event.LockerAssigned; import kr.ac.kookmin.stream.event.domain.locker.domain.Locker; import kr.ac.kookmin.stream.event.domain.locker.domain.LockerApplication; import kr.ac.kookmin.stream.event.domain.locker.domain.LockerApplicationResult; @@ -25,6 +27,7 @@ class LockerApplicationServiceImpl implements LockerApplicationService { private final LockerRepository lockerRepository; private final LockerApplicationRepository lockerApplicationRepository; + private final OutboxWriter outboxWriter; @Override @Transactional @@ -35,6 +38,7 @@ public LockerApplicationResult apply(Long memberId, LockerApplyCommand command) LockerApplication application = lockerApplicationRepository.save( LockerApplication.create(period.getId(), memberId, locker.getId(), LocalDateTime.now()) ); + outboxWriter.record(new LockerAssigned(memberId, period.getId())); return new LockerApplicationResult(application, locker, period); } diff --git a/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java b/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java index 9a0cd7d9..cee547a3 100644 --- a/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java +++ b/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java @@ -58,7 +58,8 @@ private static Locker locker(LockerStatus status) { } private static LockerApplicationServiceImpl service(FakeLockerRepository repository) { - return new LockerApplicationServiceImpl(repository, repository.applications); + // 아웃박스 기록은 이 테스트의 관심사가 아니라 아무것도 하지 않는다 + return new LockerApplicationServiceImpl(repository, repository.applications, event -> {}); } private static LockerApplyCommand command() { diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/service/impl/NotificationEventListener.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/service/impl/NotificationEventListener.java new file mode 100644 index 00000000..f860f4d6 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/service/impl/NotificationEventListener.java @@ -0,0 +1,46 @@ +package kr.ac.kookmin.stream.member.domain.notification.service.impl; + +import java.util.List; +import kr.ac.kookmin.stream.common.event.EventApplicationCanceled; +import kr.ac.kookmin.stream.common.event.EventApplied; +import kr.ac.kookmin.stream.common.event.LockerAssigned; +import kr.ac.kookmin.stream.common.event.NoticeCreated; +import kr.ac.kookmin.stream.member.domain.notification.domain.NotificationType; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 다른 도메인의 이벤트를 알림으로 바꾼다. 아웃박스 릴레이가 트랜잭션 밖에서 발행하므로 {@code @EventListener}로 받는다. + * 예외를 던지면 릴레이가 같은 이벤트를 다시 발행한다. + */ +@Component +@RequiredArgsConstructor +class NotificationEventListener { + + private final NotificationDispatcher notificationDispatcher; + + @EventListener + public void on(EventApplied event) { + notificationDispatcher.dispatch( + NotificationType.EVENT_APPLIED, List.of(event.eventTitle()), event.eventId(), List.of(event.memberId())); + } + + @EventListener + public void on(EventApplicationCanceled event) { + notificationDispatcher.dispatch( + NotificationType.EVENT_APPLICATION_CANCELED, List.of(event.eventTitle()), event.eventId(), List.of(event.memberId())); + } + + @EventListener + public void on(LockerAssigned event) { + notificationDispatcher.dispatch( + NotificationType.LOCKER_ASSIGNED, List.of(), event.lockerPeriodId(), List.of(event.memberId())); + } + + // 공지는 활성 회원 전체에게 간다 + @EventListener + public void on(NoticeCreated event) { + notificationDispatcher.dispatchToAll(NotificationType.NOTICE_CREATED, List.of(), event.noticeId(), List.of()); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java index f5d5d39f..8597370c 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java @@ -4,6 +4,8 @@ import java.time.temporal.ChronoUnit; import kr.ac.kookmin.stream.common.BusinessException; import kr.ac.kookmin.stream.common.CursorSliceResult; +import kr.ac.kookmin.stream.common.OutboxWriter; +import kr.ac.kookmin.stream.common.event.NoticeCreated; import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCategory; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCreateCommand; @@ -21,6 +23,7 @@ class NoticeServiceImpl implements NoticeService { private final NoticeRepository noticeRepository; + private final OutboxWriter outboxWriter; @Override @Transactional(readOnly = true) @@ -39,9 +42,13 @@ public Notice getNotice(Long id) { .orElseThrow(() -> new BusinessException(NoticeErrorCode.NOTICE_NOT_FOUND)); } + // 공지 저장과 알림 이벤트 기록이 함께 커밋되도록 트랜잭션을 건다 @Override + @Transactional public Notice create(Long createdBy, NoticeCreateCommand command) { - return noticeRepository.save(Notice.create(createdBy, command)); + Notice notice = noticeRepository.save(Notice.create(createdBy, command)); + outboxWriter.record(new NoticeCreated(notice.getId())); + return notice; } @Override