[BE-06] 게스트 초대 (초대·수락·거절·취소)

작업 내용 (설계 의도)

변경 사항

근거 TDD: 20260704-채팅시스템고도화-tdd.md (FR-11/12/13/14, RoomInvitation 상태 전이·초대 멱등).

방장이 특정 방에 정회원을 게스트로 초대하는 수명주기를 신설한다. 수락 시 게스트 참여자로 추가.

  • domain/message/entity/RoomInvitation.kt (신규): 필드 room/inviterUserId/inviteeUserId/status/canSpeak/expiresAt. 팩토리 create(...). 전이 메서드 accept()/reject()/revoke()/expire() — terminal 재전이 거부(InvitationStatus.canTransitTo 사용).
  • domain/message/repository/RoomInvitationRepository.kt (+ Jpa/Impl 신규): save, findById, findPendingBy(roomId, inviteeUserId), findPendingByInvitee(inviteeUserId): List (초대 수신함).
  • domain/message/service/GuestInvitationDomainService.kt (신규): invite(...) — 방장 검증 + 동일 (room,invitee) PENDING 존재 시 기존 반환(멱등). accept(invitationId, userId) — 수락자=invitee 검증 → 상태 전이 → RoomParticipant.forGuest 추가. reject, revoke(호출자=방장). findMyPendingInvitations(userId) (수신함 조회).
  • application/message/usecase/: InviteGuestUseCase, AcceptInvitationUseCase, RejectInvitationUseCase, ListMyInvitationsUseCase (신규).
  • application/message/dto/InvitationResponse.kt (신규): TDD “응답 DTO 필드 스키마”의 InvitationResponse 표대로 — id/roomId/inviterUserId/inviteeUserId/status/canSpeak/expiresAt/createdAt. of(RoomInvitation) 팩토리.
  • presentation/message/controller/InvitationApiController.kt (신규): POST /rooms/{roomId}/invitations, GET /rooms/invitations/me(내 PENDING 초대 목록, FR-12), POST /rooms/invitations/{id}/accept, .../reject. 인증: @AuthenticationPrincipal UserPrincipal로 userId(Bearer JWT). /rooms/** 인증 승격은 BE-12가 수행하므로 본 컨트롤러는 principal 기반으로 작성.
  • 게스트 발화 가드: 수락으로 추가된 게스트는 canSpeak/expiresAt을 가지며 발화 시 BE-03b의 validateCanSpeak/NotExpired 적용.
  • 롤백: 신규 테이블·엔티티라 코드 롤백만으로 안전. 초대 기능은 커뮤니티 플래그와 무관하게 /rooms 하위 additive.

의존

  • BE-03b (RoomParticipant.forGuest, 게스트 필드)
  • BE-02 (InvitationStatus)

다이어그램

처리 흐름

sequenceDiagram
    participant Host
    participant Ctl as InvitationApiController
    participant DS as GuestInvitationDomainService
    participant Guest
    Host->>Ctl: POST /rooms/1/invitations
    Ctl->>DS: invite(roomId,invitee,canSpeak,expiresInDays)
    DS-->>Ctl: PENDING invitation
    Guest->>Ctl: POST /rooms/invitations/9/accept
    Ctl->>DS: accept → forGuest 참여자 추가

테스트 케이스

  • 방장이 비멤버 정회원을 초대하면 PENDING 초대가 생성된다.
  • 동일 (room, invitee)로 PENDING 상태에서 재초대하면 기존 초대가 반환된다 (멱등).
  • 초대 수락 시 게스트가 canSpeak·expiresAt을 가진 참여자로 추가된다.
  • 초대 거절 시 초대가 REJECTED가 되고 참여자로 추가되지 않는다.
  • 이미 ACCEPTED된 초대를 다시 accept하면 거부된다 (terminal 보호).
  • 방장이 아닌 사용자가 초대를 시도하면 거부된다 (초안: 방장만 초대 가능).
  • 초대 대상이 아닌 사용자가 accept를 호출하면 거부된다.
  • GET /rooms/invitations/me는 내가 받은 PENDING 초대만 반환하고, 종료된(ACCEPTED/REJECTED/EXPIRED/REVOKED) 초대는 제외한다.
  • 인증 토큰(Bearer) 없이 초대 API 호출 시 401이 반환된다.