Skip to content

[Feat/#116] 행사·사물함·반납 예약 알림 발송 - #121

Merged
tnals0924 merged 6 commits into
feat/#115-instant-notificationsfrom
feat/#116-scheduled-notifications
Oct 18, 2026
Merged

tnals0924 merged 6 commits into
feat/#115-instant-notificationsfrom
feat/#116-scheduled-notifications

Conversation

@tnals0924

@tnals0924 tnals0924 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

정해진 시각에 가는 예약 알림 6종을 보냅니다. 모든 시각은 KST입니다.

알림 스케줄 대상 선정 수신자
신청한 행사 임박 매일 10:00 게시된 행사 중 시작 일시가 내일 신청자 (없으면 기록 안 함)
행사 모집 마감 임박 매일 10:00 게시된 행사 중 신청 마감 일시가 내일이고 지금 모집 중(calculateRecruitStatus == OPEN) 활성 회원 − 신청자
사물함 모집 마감 임박 매일 10:00 게시된 회차 중 신청 마감 일시가 내일이고 신청 기간이 시작됨 활성 회원 − 그 회차 신청자
사물함 이용 기간 만료 임박 매일 10:00 게시된 회차 중 이용 종료일이 3일 뒤 그 회차 신청자 (없으면 기록 안 함)
반납 기한 임박 10분마다 확인, 대여 1건당 1회 "반납 기한 − 1시간"이 직전 실행 이후 지금까지 사이 대여자
반납 연체 평일 10:00 (토·일 제외) 반납 기한이 지난 대여 중 이력 (반납할 때까지 평일마다) 대여자
  • 반납 기한 임박은 반납 기한 1시간 전에 한 번만 갑니다. 10분은 그 시각이 지났는지 확인하는 주기이고, 알림이 10분마다 반복되지는 않습니다. 반납 마감 시각(return_deadline)이 물품마다 달라 고정 시각 cron 하나로는 보낼 수 없어 이렇게 했습니다.

❓ 왜 해결해야 하나요?

#120까지는 사용자 행동 직후 알림만 있고, 시간이 지나서 가야 하는 알림(전날 안내, 마감 임박, 반납 기한)은 보내는 곳이 없었습니다.

⭐ 어떻게 해결했나요?

  • 이벤트 6종 (common.event): EventStartingTomorrow, EventApplyClosingTomorrow, LockerApplyClosingTomorrow, LockerUsageEndingSoon, RentalDueSoon, RentalOverdue. 신청자 기준 알림은 신청자 ID 목록을 싣고, "미신청자" 알림은 신청자 ID를 제외 목록으로 실어 member 쪽이 "활성 회원 − 신청자"를 계산합니다(event 모듈은 회원 전체를 모름).
  • 스케줄러 (각 도메인 service.impl, package-private): 자기 데이터로 대상을 고르고 OutboxWriter로 기록만 합니다. 메서드에 @Transactional(아웃박스 기록은 트랜잭션 필수), cron에 zone = "Asia/Seoul".
    • EventNotificationScheduler (event 모듈)
    • LockerNotificationScheduler (event 모듈)
    • RentalNotificationScheduler (welfare 모듈). 반납 기한은 기존 RentalRecord.dueAt()(물품 반납 정책 기준)으로 계산합니다. 반납 정책이 없는 물품은 기한을 계산할 수 없어 빠집니다.
  • 조회 추가
    • EventRepository: 시작·마감 일시 구간으로 게시 행사 조회, 유효 신청자 ID
    • LockerRepository·LockerApplicationRepository: 마감 일시 구간·이용 종료일로 게시 회차 조회, 회차 신청자 ID
    • RentalHistoryRepository.findAllByStatus, RentalHistoryService.getAllReturnRequiredRentals()(기존 회원별 조회와 같은 짝짓기 로직)
  • 리스너: NotificationEventListener에 6종 메서드를 추가했습니다.
  • 문서: architecture.md 6-2절에 예약 이벤트 스케줄러 위치·규칙을 추가했습니다.
  • 기존 테스트 수정: EventServiceImplTest, LockerServiceImplTest, LockerApplicationServiceImplTest의 가짜 저장소가 포트를 직접 구현해서, 새 메서드에 기존 방식대로 UnsupportedOperationException 스텁을 추가했습니다.

🧩 이 PR의 한계 & 트레이드오프

  • 놓친 회차는 다시 보내지 않음: 발송 기록을 남기지 않고 날짜·시간 구간으로 고릅니다. 서버가 10시나 10분 경계에 내려가 있으면 그 회차 알림은 빠집니다.
  • 반납 기한 임박 구간: 지금 시각을 10분 단위로 내린 시각까지를 봅니다. 스케줄러 스레드가 밀려 실행이 10분 이상 늦어지면 그 사이 구간이 빠질 수 있습니다(스케줄러 풀 크기 기본값 1, 릴레이와 공유).
  • 반납 연체는 평일 하루 한 번 10시: 문구표의 "1시간 경과 시 1회씩" 대신 결정에 따라 평일 10시에만 보냅니다(cron MON-FRI). 금요일 10시 이후에 연체된 대여는 월요일 10시에 첫 알림이 갑니다. 공휴일은 따로 거르지 않아 평일 공휴일에는 보냅니다.
  • 사물함 모집 기간을 서버가 검사하지 않음: 사물함 신청 API는 apply_end_at을 확인하지 않아, "내일 마감" 알림 뒤에도 신청이 막히지 않습니다(기존 동작, 이번 범위 아님).
  • 대여 중 이력 전체 조회: 10분마다 RENTAL 상태 이력 전체와 물품을 읽습니다. rental_status 인덱스가 없지만 대여 건수가 적어 두지 않았습니다.
  • 로컬 검증 범위: 컴파일, 전체 단위 테스트, ModularityTests·DomainImplAccessTests까지 확인했습니다. 로컬에 DB가 없어 새 JPQL·파생 쿼리와 스케줄 실행은 dev에서 확인해야 합니다(대상 행의 날짜를 내일로 바꾼 뒤 10시 실행 확인, 또는 일시적으로 cron을 바꿔 확인).

⛓️ 기존 기능에 미치는 영향

  • 기존 API는 그대로입니다. 스케줄 작업 3개가 추가되고, 그 시각에 조회 쿼리와 outbox_events INSERT가 생깁니다.
  • RentalHistoryService에 조회 메서드가 하나 늘었습니다.

🔀 Edge Case & 실패 시나리오

상황 동작
내일 시작하는 행사에 신청자 없음 기록 안 함
마감이 내일이지만 강제 마감·정원 마감 기록 안 함
사물함 회차 신청 기간이 아직 시작 전 마감 임박 기록 안 함
반납 정책이 없는 물품 기한 계산 불가로 제외
반납 기한이 이미 지난 뒤 처음 실행 기한 임박은 구간 밖이라 안 보내고, 다음 평일 10시에 연체 알림
토·일요일 연체 알림 안 보냄, 월요일 10시에 보냄 (기한 임박은 주말에도 보냄)
스케줄 중 DB 오류 그 회차 트랜잭션 롤백, 기록 없음(다음 회차는 다른 대상)

📋 검토한 대안과 선택 이유

  • 발송 기록 테이블로 정확히 한 번 보장: 놓친 회차도 다시 보낼 수 있지만 테이블과 쿼리가 늘어 이번에는 구간 방식으로 정했습니다.
  • 별도 배치 모듈에서 여러 도메인 조합: 새 모듈과 의존 규칙이 필요해, 각 도메인이 자기 데이터로 대상만 고르고 회원 쪽 계산은 구독 측에 맡겼습니다.
  • 반납 연체 매시간 발송: 야간·주말에 매시간 알림이 가서 하루 한 번으로 정했고, 주말에는 보내지 않습니다.
  • 반납 기한 임박을 매시 정각에 확인: 확인 횟수는 줄지만 마감 시각이 정각이 아닌 물품(예: 18:30)은 1시간 전이 아니라 30분 전에 가서, 10분 간격 확인을 유지했습니다.

💬 리뷰 포인트

  • 반납 기한 임박의 10분 구간 계산(floorToCheckInterval)
  • 행사·사물함 마감 임박 대상 조건(모집 중, 신청 기간 시작 여부)
  • 이벤트에 신청자 ID 목록을 싣는 방식 (신청자가 많으면 payload가 커짐)

@tnals0924 tnals0924 self-assigned this Oct 14, 2026
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 50 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: billilge/stream-server/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 777e9b89-9998-421e-bc7d-9c05fe06c114
📥 Commits

Reviewing files that changed from the base of the PR and between 7b7ac96 and 8e60aa3.

📒 Files selected for processing: 29
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplyClosingTomorrow.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventStartingTomorrow.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerApplyClosingTomorrow.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerUsageEndingSoon.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/RentalDueSoon.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/RentalOverdue.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/repository/EventRepository.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventNotificationScheduler.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerApplicationRepository.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerRepository.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerNotificationScheduler.java
  • core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventServiceImplTest.java
  • core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java
  • core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImplTest.java
  • core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/service/impl/NotificationEventListener.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/repository/RentalHistoryRepository.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/RentalHistoryService.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/impl/RentalHistoryServiceImpl.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/impl/RentalNotificationScheduler.java
  • docs/conventions/architecture.md
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventRepositoryImpl.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationRepositoryImpl.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerPeriodJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerRepositoryImpl.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalHistoryJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalHistoryRepositoryImpl.java
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tnals0924
tnals0924 added this pull request to stack #122 October 14, 2026 07:31
@tnals0924
tnals0924 force-pushed the feat/#116-scheduled-notifications branch 2 times, most recently from 22d0521 to 2adbdb3 Compare October 18, 2026 14:57
private final EventRepository eventRepository;
private final OutboxWriter outboxWriter;

@Scheduled(cron = "0 0 10 * * *", zone = "Asia/Seoul")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cron으로 고정 시각 표현한 점 이해했습니다.

@EventListener
public void on(EventStartingTomorrow event) {
notificationDispatcher.dispatch(
NotificationType.EVENT_STARTING_TOMORROW, List.of(event.eventTitle()), event.eventId(), event.applicantIds());

@jjunh33 jjunh33 Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event 모듈에서는 전체 member를 참조할 수 없기 때문에, member 모듈에서 신청자 ID를 받고 전체에서 제외하는 형식으로 알림 발송하는 점 이해했습니다!

List<LockerPeriod> periods = lockerRepository.findPublishedPeriodsApplyEndingBetween(
tomorrow.atStartOfDay(), tomorrow.plusDays(1).atStartOfDay());
for (LockerPeriod period : periods) {
if (now.isBefore(period.getApplyStartAt())) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금은 사물함 신청이 다 찬 상태에서도 마감임박 알림이 가는 것으로 보이는데, 가능하다면 이 조건에 정원 마감 관련 조건도 추가해서 이미 정원이 다 찬 상태에서는 알림이 안 가도록 하는 것은 어떨까요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정원 마감을 확인하려면 모든 사물함이 다 마감되었는지를 확인해야 하기 때문에 이 정도는 정책상 어쩔 수 없는 부분으로 넘기는 게 맞을 거 같습니다!

@Scheduled(cron = "0 0 10 * * *", zone = "Asia/Seoul")
@Transactional
public void recordDailyNotifications() {
LocalDateTime now = LocalDateTime.now(DateUtil.KST);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#112 PR에서 JVM 기본 시간대를 서울로 고정했으니 LocalDateTime.now(DateUtil.KST)는 LocalDateTime.now()와 같은 값이라, 그냥 now()로 써도 될 것 같습니다. 그리고 현재 저장소 전체에서 이 부분이 정해지지 않고 코드마다 다르게 되어있어서, 컨벤션 규약으로 정해서 어느 쪽을 쓸지 팀 규칙으로 정해두면 좋을 것 같습니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 이미 많은 곳에서 이렇게 쓰이고 있어서 추후에 이슈를 새로 파서 진행하는 게 좋을 거 같아요. 그래서 일부러 따로 언급하지 않고 있었는데 앞으로는 다들 이거 주의해서 코드 검토해주세요~

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 확인했습니다 이번 열린피드백 페이지에서도 수정해서 커밋 올리겠습니다!

public void on(RentalOverdue event) {
notificationDispatcher.dispatch(
NotificationType.RENTAL_OVERDUE, List.of(event.itemName()), event.rentalHistoryId(), List.of(event.memberId()));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applicatIds가 dispatch에서는 받을 사람으로 쓰이고 dispatchToAll에서는 뺄 사람으로 쓰이는것으로 이해했습니다!!

outboxWriter.record(new EventStartingTomorrow(event.getId(), event.getTitle(), applicantIds));
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구조상 event 모듈에서 member를 볼 수 없어서 스케줄러가 신청자 ID만 싣고 넘기는 구조 이해했습니다.

@tnals0924
tnals0924 force-pushed the feat/#116-scheduled-notifications branch from 2adbdb3 to 98412f5 Compare October 18, 2026 15:59
@tnals0924
tnals0924 merged commit 1b7d7df into main Oct 18, 2026
1 check passed
@tnals0924
tnals0924 deleted the feat/#116-scheduled-notifications branch October 18, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

행사·사물함·반납 예약 알림 발송

4 participants