[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 멤버가 아니므로 멤버십 범위 조회에서 거부된다.