[BE-05] 읽음 커서 + 안읽은 수
작업 내용 (설계 의도)
변경 사항
근거 TDD: 20260704-채팅시스템고도화-tdd.md (FR-7/9, 읽음 커서 경합 last-write-wins forward-only).
참여자별 읽음 커서를 갱신하고 안읽은 수를 집계한다. 멀티 디바이스 경합은 forward-only 단조 증가로 처리(Non-Goal: 정교한 기기 동기화 제외).
domain/message/service/ReadCursorDomainService.kt(신규):markRead(roomId, userId, lastReadMessageId)— 참여자 조회 →markReadUpTo→ 저장 → 읽음 브로드캐스트(MessageBroadcastGateway.broadcastRead).unreadCount(roomId, userId),unreadForMyRooms(userId).domain/message/repository/MessageCustomRepository.kt+ Impl:countUnread(roomId, afterMessageId, excludeUserId): Long추가 (id > afterMessageId AND user_id != me AND deleted_at IS NULL). — MessageCustomRepository 계열은 본 티켓 단독 수정.application/message/usecase/MarkReadUseCase.kt,GetMyUnreadUseCase.kt(신규): DomainService만 호출, execute 10줄 이내.application/message/dto/RoomUnreadResponse.kt(신규):{roomId, unreadCount}— TDD 응답 DTO 표대로.presentation/message/controller/ReadCursorApiController.kt(신규 파일):POST /rooms/{roomId}/read(→ RoomUnreadResponse),GET /rooms/me/unread. 인증:@AuthenticationPrincipal UserPrincipal로 userId(Bearer JWT). MessageApiController 무수정.- 상태 보호: 읽음 반영은 forward-only라 역행 요청은 무시(멱등).
의존
- BE-03b (RoomParticipant.lastReadMessageId, markReadUpTo)
- BE-04 (MessageBroadcastGateway.broadcastRead — 읽음 실시간 표시)
다이어그램
클래스 의존
flowchart LR ReadCursorApiController --> MarkReadUseCase ReadCursorApiController --> GetMyUnreadUseCase MarkReadUseCase --> ReadCursorDomainService ReadCursorDomainService --> RoomParticipantRepository ReadCursorDomainService --> MessageCustomRepository ReadCursorDomainService --> MessageBroadcastGateway
테스트 케이스
- lastReadMessageId=50 참여자가 방에 id 51~60의 상대 메시지가 있으면 unreadCount=10이다.
- 본인이 보낸 메시지는 안읽은 수에서 제외된다.
- 읽음 요청 시
/topic/rooms/{id}/read로 상대에게 읽음 이벤트가 브로드캐스트된다. - lastReadMessageId=60 상태에서 read(30)을 보내면 커서는 60으로 유지된다 (역행 무시·멱등).
- 안읽은 메시지가 0건이면 unreadCount=0을 반환한다 (빈 상태).
- soft-delete된 메시지는 안읽은 수에 포함되지 않는다.