[BE-03b] RoomParticipant 확장 (게스트·읽음 커서 필드)
작업 내용 (설계 의도)
변경 사항
근거 TDD: 20260704-채팅시스템고도화-tdd.md (FR-7/13/14, 실패 경로·동시성).
RoomParticipant에 참여 스코프 속성과 읽음 커서를 additive 추가하고, Rich Domain 메서드로 캡슐화한다.
RoomParticipant.kt:participantType: ParticipantType,canSpeak: Boolean,expiresAt: ZonedDateTime?,lastReadMessageId: Long?추가.- 팩토리
create(room, userId)는 MEMBER·canSpeak=true·expiresAt=null 유지(기존 호환),forGuest(room, userId, canSpeak, expiresInDays)추가 (시간은 메서드 내부ZonedDateTime.now()로 해결 — no-time-parameter). markReadUpTo(messageId): forward-only 단조 증가(lastReadMessageId == null || messageId > lastReadMessageId일 때만 갱신).validateCanSpeak(): 읽기 전용 게스트면 예외.validateNotExpired():expiresAt != null && now > expiresAt이면 예외.evict(): 방출 표시(soft-delete는 DomainService가 수행).
- 팩토리
RoomParticipantRepository.kt+RoomParticipantCustomRepository.kt:findExpiredGuestsBefore(threshold): List,findActiveByUserId(userId): List추가.- infrastructure impl 2종에 QueryDSL 구현.
- 파일 범위: RoomParticipant.kt, RoomParticipantRepository.kt, RoomParticipantCustomRepository.kt + impl 2종. Room·MessageDomainService 무수정.
- 롤백: BE-01 nullable/DEFAULT 컬럼이라 코드 롤백만으로 안전.
의존
- BE-01 (room_participants 확장 컬럼)
- BE-02 (ParticipantType)
다이어그램
클래스 의존
flowchart LR RoomParticipant --> ParticipantType RoomParticipantRepository --> RoomParticipant RoomParticipantCustomRepository --> RoomParticipant RoomParticipantRepositoryImpl -.implements.-> RoomParticipantRepository
테스트 케이스
forGuest(room, 5, canSpeak=true, expiresInDays=7)는 GUEST·expiresAt=now+7d 참여자를 만든다.markReadUpTo(100)후markReadUpTo(50)을 호출해도 lastReadMessageId는 100으로 유지된다 (forward-only).- 읽기 전용 게스트(canSpeak=false)의
validateCanSpeak()는 예외를 던진다. - expiresAt이 과거인 게스트의
validateNotExpired()는 예외를 던진다. - 만료 시각이 지나지 않은 게스트는
findExpiredGuestsBefore(now)에 포함되지 않는다. - MEMBER 참여자는 expiresAt=null이라
findExpiredGuestsBefore에서 제외된다.