[BE-07] 게스트 만료 방출 배치 + 수동 방출

작업 내용 (설계 의도)

변경 사항

근거 TDD: 20260704-채팅시스템고도화-tdd.md (FR-14/15, 배치 방출 + 매 요청 만료 가드, 배치 부분 실패).

만료된 게스트를 주기적으로 방출하고, 방장의 수동 방출을 지원한다. 배치 지연 구간의 발화·열람은 BE-03b 요청 가드가 차단하므로 이중 방어.

  • domain/message/service/GuestEvictionDomainService.kt (신규): evictExpired()findExpiredGuestsBefore(now) → 참여자별 독립 soft-delete(부분 실패는 로깅 후 계속). 읽은 이력(lastReadMessageId) 유지. evict(roomId, userId, requesterId) — 방장 검증 후 방출(FR-15).
  • application/message/usecase/: ExpireGuestsUseCase(배치용), EvictGuestUseCase(수동) (신규).
  • presentation/message/scheduler/GuestExpiryScheduler.kt (신규): @Scheduled(cron) → ExpireGuestsUseCase. 실패 시 NotificationChannelGateway로 알림(Operations 요구사항).
  • presentation/message/controller/GuestEvictionApiController.kt (신규): POST /rooms/{roomId}/guests/{userId}/evict.
  • 롤백: 스케줄러는 @Value("\${chat.guest.expiry.enabled:true}") 코드 기본값 플래그로 OFF 가능 — application.yml 미수정(wave3의 yml 단독 수정자는 BE-08). soft-delete라 데이터 손실 없음.

의존

  • BE-03b (findExpiredGuestsBefore, expiresAt)

다이어그램

처리 흐름

sequenceDiagram
    participant S as GuestExpiryScheduler
    participant UC as ExpireGuestsUseCase
    participant DS as GuestEvictionDomainService
    participant N as NotificationChannelGateway
    S->>UC: execute() (cron)
    UC->>DS: evictExpired()
    DS->>DS: findExpiredGuestsBefore(now), soft-delete each
    DS-->>UC: evictedCount
    UC-->>S: ok / 실패 시 N.notify

테스트 케이스

  • expiresAt이 지난 게스트는 배치 실행 시 방출(soft-delete)된다.
  • 방출된 게스트의 lastReadMessageId(읽은 이력)는 유지된다.
  • 만료되지 않은 게스트·MEMBER는 배치에서 방출되지 않는다.
  • 참여자 1건 처리 실패 시 나머지 게스트는 계속 방출되고 실패는 로깅된다 (부분 실패).
  • 방장이 만료 전 게스트를 수동 방출하면 즉시 방출된다 (FR-15).
  • 방장이 아닌 사용자의 수동 방출은 거부된다.
  • 배치가 예외로 실패하면 NotificationChannelGateway 알림이 발송된다.