[BE-08] 커뮤니티 도메인 (개설·가입·역할·승인)

작업 내용 (설계 의도)

변경 사항

근거 TDD: 20260704-채팅시스템고도화-tdd.md (FR-1/2/3, 바운디드 컨텍스트 분리 판단, CommunityMember 상태 전이).

동아리·모임을 신규 바운디드 컨텍스트 domain/community로 분리한다. 독립 라이프사이클·독립 데이터 소유·다른 변경 주기 → 신규 도메인 채택(기존 message 도메인 합류 미채택: 채팅과 멤버십은 변경 주기·책임이 다름). 도메인 간 참조는 ID·이벤트만.

  • domain/community/entity/Community.kt (신규): name/description/visibility/sportCategory/hostUserId. 팩토리 create(...) — 개설 시 CommunityCreatedEvent(hostUserId 포함) 적재. isPublic(), transferHostTo(newHostUserId).
  • domain/community/entity/CommunityMember.kt (신규): communityId/userId/role/status. join(public→ACTIVE+MemberJoined, private→PENDING_APPROVAL), approve(), kick()(대상 HOST 거부), leave()(HOST는 위임 전 거부). Join/Left 이벤트 적재.
  • domain/community/event/: CommunityCreatedEvent, CommunityMemberJoinedEvent, CommunityMemberLeftEvent (AbstractDomainEvent, topic=null).
  • domain/community/repository/: CommunityRepository(save/findById/findPublicByKeyword/findByMemberUserId), CommunityMemberRepository(save/findActiveBy/findActiveByCommunityId) (+ Custom) — TDD 시그니처. infrastructure impl(JPA/QueryDSL) 신규.
  • domain/community/service/CommunityDomainService.kt (신규): create/join/approve/kick/transfer/leave + 조회 getCommunity(id, requesterId)/findPublicCommunities(keyword)/findMembers(id, requesterId)/findMyCommunities(userId) + requireActiveMember(communityId, requesterId) 인가 가드. DomainEventPublisher로 이벤트 발행.
  • domain/community/exception/NotCommunityMemberException.kt (신규): FR-13 ② 인가 실패 → 403.
  • FR-13 ② 멤버십 범위 조회 인가 (서버 강제): findMembers(id, requesterId)requireActiveMember(id, requesterId)로 요청자가 해당 커뮤니티 ACTIVE 멤버인지 검증(비멤버·게스트 거부). getCommunity(id, requesterId)는 공개 커뮤니티면 통과, 비공개면 requireActiveMember 적용. 게스트는 컨텍스트 방(contextType=COMMUNITY) 참여자일 뿐 community_members에 ACTIVE 레코드가 없어 findActiveBy(communityId, userId)==null → 거부(contextId=communityId 우회 차단). FE-12 UI 게이팅에 의존하지 않는다.
  • application/community/usecase/: CreateCommunity/JoinCommunity/ApproveMember/KickMember/TransferHost/LeaveCommunity + GetCommunity/ListPublicCommunities/ListCommunityMembers/ListMyCommunities UseCase (신규, 조회 GET 4종).
  • application/community/dto/: CommunityResponse(id/name/description/visibility/sportCategory/hostUserId/memberCount/roomId/createdAt), CommunityMemberResponse(id/communityId/userId/role/status/joinedAt) — TDD “응답 DTO 필드 스키마” 표대로. memberCount는 조회 시 findActiveByCommunityId().size 집계, roomId는 컨텍스트 방(findByContext(COMMUNITY,id)) 없으면 null.
  • presentation/community/controller/CommunityApiController.kt (신규): TDD REST 계약 — POST 6종 + GET 4종(GET /communities?keyword=, GET /communities/{id}, GET /communities/{id}/members, GET /communities/me). 인증: @AuthenticationPrincipal UserPrincipal로 userId(Bearer JWT), X-User-Id 미사용.
  • SecurityConfig.kt: 본 티켓은 수정하지 않는다/communities/** authenticated() 규칙은 BE-04(wave2)가 선등록. 컨트롤러만 추가.
  • application.yml: chat.community.enabled 플래그 — wave3의 yml 단독 수정자(BE-07은 @Value 코드 기본값, BE-12는 yml 미수정).
  • 롤백: 신규 도메인·테이블. 플래그 OFF로 엔드포인트 비활성, 기존 기능 무영향.

의존

  • BE-01 (communities, community_members 테이블)
  • BE-02 (CommunityVisibility/Role/MembershipStatus/SportCategory)

다이어그램

클래스 의존

flowchart LR
    CommunityApiController --> UseCases
    UseCases --> CommunityDomainService
    CommunityDomainService --> CommunityRepository
    CommunityDomainService --> CommunityMemberRepository
    CommunityDomainService --> DomainEventPublisher
    Community --> CommunityCreatedEvent
    CommunityMember --> CommunityMemberJoinedEvent

테스트 케이스

  • 공개 커뮤니티 개설 후 가입하면 즉시 ACTIVE 멤버가 된다 + MemberJoined 이벤트 발행.
  • 비공개 커뮤니티 가입은 PENDING_APPROVAL이 되고, 방장 승인 후 ACTIVE가 된다.
  • 커뮤니티 개설 시 CommunityCreatedEvent(hostUserId)가 발행된다.
  • 방장이 멤버를 강퇴하면 KICKED가 되고 MemberLeft 이벤트가 발행된다.
  • 방장 강퇴 대상이 HOST 본인이면 거부된다.
  • HOST가 위임 없이 탈퇴를 시도하면 거부된다.
  • 방장 권한 위임 후 기존 방장은 MEMBER, 신규 사용자가 HOST가 된다.
  • 방장이 아닌 사용자의 승인·강퇴는 거부된다.
  • GET /communities?keyword=축구는 공개 커뮤니티 중 키워드 매칭 목록을 반환하고, 비공개는 제외된다.
  • GET /communities/{id}는 memberCount와 연결된 roomId(없으면 null)를 포함해 반환한다.
  • GET /communities/me는 내가 ACTIVE 멤버인 커뮤니티만 반환하고, 탈퇴/강퇴된 것은 제외한다.
  • GET /communities/{id}/members는 ACTIVE 멤버 목록을 role과 함께 반환한다.
  • (FR-13 ②) 커뮤니티 ACTIVE 멤버가 아닌 사용자가 GET /communities/{id}/members를 호출하면 NotCommunityMemberException(403)으로 거부된다.
  • (FR-13 ②) 컨텍스트 방(contextType=COMMUNITY)에만 초대된 게스트가 contextId(communityId)로 GET /communities/{id}/members를 호출하면 커뮤니티 멤버가 아니므로 거부된다.
  • (FR-13 ②) 비공개 커뮤니티 상세 GET /communities/{id}는 비멤버가 호출하면 거부되고, 공개 커뮤니티 상세는 비멤버도 조회 가능하다.
  • 탈퇴(LEFT)·강퇴(KICKED)된 사용자는 ACTIVE 멤버가 아니므로 멤버십 범위 조회에서 거부된다.