From 524f180886960d75125dee7e0bf53df2ac03b61e Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:36:38 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=8B=A0=EC=B2=AD=C2=B7=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kookmin/stream/common/event/EventApplicationCanceled.java | 4 ++++ .../java/kr/ac/kookmin/stream/common/event/EventApplied.java | 4 ++++ .../kr/ac/kookmin/stream/common/event/LockerAssigned.java | 4 ++++ .../java/kr/ac/kookmin/stream/common/event/NoticeCreated.java | 4 ++++ 4 files changed, 16 insertions(+) create mode 100644 core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplicationCanceled.java create mode 100644 core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplied.java create mode 100644 core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerAssigned.java create mode 100644 core/common/src/main/java/kr/ac/kookmin/stream/common/event/NoticeCreated.java 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 0000000..a2c9f91 --- /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 0000000..e01635c --- /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 0000000..a264bc6 --- /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 0000000..aea81c7 --- /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 {} From 9f01ab94adb8d7541c60ba4bc7f90c14780f0c7a Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:37:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=ED=96=89=EC=82=AC=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=C2=B7=EC=B7=A8=EC=86=8C=20=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/service/impl/EventApplicationServiceImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 f77b88e..6598dfd 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())); } /** 남의 신청은 없는 것으로 보여야 하므로 조회 단계에서 회원 식별자로 함께 거른다. */ From 1adf9d9666c500f85bc79ee4f07146b2e6af1c52 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:37:07 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EB=B0=B0=EC=A0=95=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../locker/service/impl/LockerApplicationServiceImpl.java | 4 ++++ .../locker/service/impl/LockerApplicationServiceImplTest.java | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 1f3f3c0..f766c67 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 9a0cd7d..cee547a 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() { From fa141e8594b4361dbdfd5347f7016c9411ff1b87 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:37:16 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notice/service/impl/NoticeServiceImpl.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 f5d5d39..8597370 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 From 60cb33b2e4cb235bfcb96d20fe2a19f9b55f8b46 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:37:44 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EC=A6=89=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=EB=84=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/NotificationEventListener.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/service/impl/NotificationEventListener.java 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 0000000..f860f4d --- /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()); + } +}