-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ Implementing Chatroom Join API: A Bounded Context Approach (#184
) * chore: external-api module messabe broker config 주입 * chore: add chat_join_event_message exchange properties * chore: add chat.join exchange * style: domain service 역할을 구분하기 위한 chat_member 생성 로직 위치 수정 * fix: modify the chat_member entity to don't check exsists member is_deleted * feat: add domain logic(is_active(), is_banned_member()) in the chat_member entity * feat: add ban domain method in entity * feat: add chat_member exception & error code * test: add user_fixture & chat_room_fixture within the domain test package * feat: impl chat_member_repository * feat: impl create_member business logic within chat_member_service * test: chat_member create business logic unit test * fix: exclude nickname parameter when create chat_member * feat: add password check and verify domain logic whitin the chat room entity * feat: add chat_room error code with exception * fix: add chat_room not_found error code * feat: chat_room_service.read_by_id() * feat: add count_chat_member in chat_room logic * feat: chat_member_join business service impl * feat: add chat_room_join event handler within infra module * fix: when member join finish, call the chat_room join event handler * feat: join_chat_room_usecase & fix chat_member_join_service return value * fix: chat_member_join_service return value is adding plus 1 about current_member_count * feat: impl join chat room controller * docs: write join_api swagger * fix: join_req_dto is added getter method for swagger ui presenting * fix: chat_member_repository method rule find_by_chat_room_id -> find_by_chatroom_id * test: add chat_member_fixture in the external-api module * test: chat_member_join_service_unit_test * test: refactor & add chat_member_join_service test case * chore: prevent automatic rabbitmq connection creation during application startup * fix: add two types of getter whitin chat_member_req * fix: add default constructor within dto * fix: modify distributed_lock's key correctly about the spel * rename: add log within chat_member_join_service * test: fix chat_room_id of the chat_room_fixture due to integration failure when id is fixed 1 * rename: join_service_test (usecase package) move to (service package) * fix: chat_room_join_event_hander bean create within the message_brocker_config * fix: modify join_event_hander phase after_commit to before_commit due to at_least_one condition * test: chat_member_join integration test
- Loading branch information
1 parent
7f74e8c
commit afb5d86
Showing
33 changed files
with
1,241 additions
and
22 deletions.
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/api/ChatMemberApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package kr.co.pennyway.api.apis.chat.api; | ||
|
||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.Parameters; | ||
import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import io.swagger.v3.oas.annotations.media.SchemaProperty; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import kr.co.pennyway.api.apis.chat.dto.ChatMemberReq; | ||
import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; | ||
import kr.co.pennyway.api.common.annotation.ApiExceptionExplanation; | ||
import kr.co.pennyway.api.common.annotation.ApiResponseExplanations; | ||
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails; | ||
import kr.co.pennyway.domain.domains.chatroom.exception.ChatRoomErrorCode; | ||
import kr.co.pennyway.domain.domains.member.exception.ChatMemberErrorCode; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
import org.springframework.validation.annotation.Validated; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
|
||
@Tag(name = "[채팅방 멤버 API]") | ||
public interface ChatMemberApi { | ||
@Operation(summary = "채팅방 멤버 가입", method = "POST", description = "채팅방에 멤버로 가입한다.") | ||
@Parameters({ | ||
@Parameter(name = "chatRoomId", description = "채팅방 ID", required = true, in = ParameterIn.PATH), | ||
@Parameter(name = "payload", description = "채팅방 멤버 가입 요청 DTO", required = true, in = ParameterIn.DEFAULT, schema = @Schema(implementation = ChatMemberReq.Join.class)) | ||
}) | ||
@ApiResponse(responseCode = "200", description = "채팅방 멤버 가입 성공", content = @Content(schemaProperties = @SchemaProperty(name = "chatRoom", schema = @Schema(implementation = ChatRoomRes.Detail.class)))) | ||
@ApiResponseExplanations(errors = { | ||
@ApiExceptionExplanation(value = ChatRoomErrorCode.class, constant = "INVALID_PASSWORD", summary = "비밀번호가 일치하지 않음", description = "비밀번호가 일치하지 않아 채팅방 멤버 가입에 실패했습니다."), | ||
@ApiExceptionExplanation(value = ChatRoomErrorCode.class, constant = "NOT_FOUND_CHAT_ROOM", summary = "채팅방을 찾을 수 없음", description = "채팅방을 찾을 수 없어 채팅방 멤버 가입에 실패했습니다."), | ||
@ApiExceptionExplanation(value = ChatRoomErrorCode.class, constant = "FULL_CHAT_ROOM", summary = "채팅방이 가득 참", description = "채팅방이 가득 차서 채팅방 멤버 가입에 실패했습니다."), | ||
@ApiExceptionExplanation(value = ChatMemberErrorCode.class, constant = "BANNED", summary = "차단된 사용자", description = "차단된 사용자로 채팅방 멤버 가입에 실패했습니다."), | ||
@ApiExceptionExplanation(value = ChatMemberErrorCode.class, constant = "ALREADY_JOINED", summary = "이미 가입한 사용자", description = "이미 가입한 사용자로 채팅방 멤버 가입에 실패했습니다.") | ||
}) | ||
ResponseEntity<?> joinChatRoom( | ||
@PathVariable("chatRoomId") Long chatRoomId, | ||
@Validated @RequestBody ChatMemberReq.Join payload, | ||
@AuthenticationPrincipal SecurityUserDetails user | ||
); | ||
} |
37 changes: 37 additions & 0 deletions
37
...ernal-api/src/main/java/kr/co/pennyway/api/apis/chat/controller/ChatMemberController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package kr.co.pennyway.api.apis.chat.controller; | ||
|
||
import kr.co.pennyway.api.apis.chat.api.ChatMemberApi; | ||
import kr.co.pennyway.api.apis.chat.dto.ChatMemberReq; | ||
import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; | ||
import kr.co.pennyway.api.apis.chat.usecase.ChatMemberUseCase; | ||
import kr.co.pennyway.api.common.response.SuccessResponse; | ||
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.access.prepost.PreAuthorize; | ||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
import org.springframework.validation.annotation.Validated; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
@Slf4j | ||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/v2/chat-rooms/{chatRoomId}/chat-members") | ||
public class ChatMemberController implements ChatMemberApi { | ||
private static final String CHAT_ROOM = "chatRoom"; | ||
private final ChatMemberUseCase chatMemberUseCase; | ||
|
||
@Override | ||
@PostMapping("") | ||
@PreAuthorize("isAuthenticated()") | ||
public ResponseEntity<?> joinChatRoom( | ||
@PathVariable("chatRoomId") Long chatRoomId, | ||
@Validated @RequestBody ChatMemberReq.Join payload, | ||
@AuthenticationPrincipal SecurityUserDetails user | ||
) { | ||
ChatRoomRes.Detail detail = chatMemberUseCase.joinChatRoom(user.getUserId(), chatRoomId, payload.password()); | ||
|
||
return ResponseEntity.ok(SuccessResponse.from(CHAT_ROOM, detail)); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatMemberReq.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package kr.co.pennyway.api.apis.chat.dto; | ||
|
||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import jakarta.validation.constraints.Pattern; | ||
|
||
public final class ChatMemberReq { | ||
@Schema(title = "채팅방 멤버 가입 요청 DTO") | ||
public static class Join { | ||
@Schema(description = "채팅방 비밀번호. NULL을 허용한다. 비밀번호는 6자리 정수만 허용", example = "123456") | ||
@Pattern(regexp = "^[0-9]{6}$", message = "채팅방 비밀번호는 6자리 정수여야 합니다.") | ||
private String password; | ||
|
||
protected Join() { | ||
} | ||
|
||
public Join(String password) { | ||
this.password = password; | ||
} | ||
|
||
// 메서드 표현 일관성을 유지하고, password를 Integer로 변환하여 반환하는 getter | ||
public Integer password() { | ||
return password != null ? Integer.valueOf(password) : null; | ||
} | ||
|
||
// Swagger UI에서 표현하기 위한 getter | ||
public String getPassword() { | ||
return password; | ||
} | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
...xternal-api/src/main/java/kr/co/pennyway/api/apis/chat/service/ChatMemberJoinService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package kr.co.pennyway.api.apis.chat.service; | ||
|
||
import kr.co.pennyway.domain.common.redisson.DistributedLock; | ||
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; | ||
import kr.co.pennyway.domain.domains.chatroom.exception.ChatRoomErrorCode; | ||
import kr.co.pennyway.domain.domains.chatroom.exception.ChatRoomErrorException; | ||
import kr.co.pennyway.domain.domains.chatroom.service.ChatRoomService; | ||
import kr.co.pennyway.domain.domains.member.domain.ChatMember; | ||
import kr.co.pennyway.domain.domains.member.service.ChatMemberService; | ||
import kr.co.pennyway.domain.domains.user.domain.User; | ||
import kr.co.pennyway.domain.domains.user.exception.UserErrorCode; | ||
import kr.co.pennyway.domain.domains.user.exception.UserErrorException; | ||
import kr.co.pennyway.domain.domains.user.service.UserService; | ||
import kr.co.pennyway.infra.common.event.ChatRoomJoinEvent; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.tuple.Pair; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Slf4j | ||
@Service | ||
@RequiredArgsConstructor | ||
public class ChatMemberJoinService { | ||
private static final long MAX_MEMBER_COUNT = 300; | ||
|
||
private final UserService userService; | ||
private final ChatRoomService chatRoomService; | ||
private final ChatMemberService chatMemberService; | ||
|
||
private final ApplicationEventPublisher eventPublisher; | ||
|
||
/** | ||
* 사용자가 채팅방에 참여하는 도메인 비즈니스 로직을 처리한다. | ||
* 채팅방 가입 가능 여부 확인을 위해 현재 가입한 회원 수를 조회하는데, 이 때 분산 락을 걸어 동시성 문제를 해결한다. | ||
* | ||
* @param userId Long : 가입하려는 사용자의 ID | ||
* @param chatRoomId Long : 가입하려는 채팅방의 ID | ||
* @param password Integer : 비공개 채팅방의 경우 비밀번호 정보를 입력받으며, 채팅방에 비밀번호가 없을 경우 null | ||
* @return Pair<ChatRoom, Integer> - 채팅방 정보와 현재 가입한 회원 수 | ||
*/ | ||
@DistributedLock(key = "'chat-room-join-' + #chatRoomId") | ||
public Pair<ChatRoom, Integer> execute(Long userId, Long chatRoomId, Integer password) { | ||
ChatRoom chatRoom = chatRoomService.readChatRoom(chatRoomId).orElseThrow(() -> new ChatRoomErrorException(ChatRoomErrorCode.NOT_FOUND_CHAT_ROOM)); | ||
|
||
Long currentMemberCount = chatMemberService.countActiveMembers(chatRoomId); | ||
if (isFullRoom(currentMemberCount)) { | ||
log.warn("채팅방이 가득 찼습니다. chatRoomId: {}", chatRoomId); | ||
throw new ChatRoomErrorException(ChatRoomErrorCode.FULL_CHAT_ROOM); | ||
} | ||
|
||
if (chatRoom.isPrivateRoom() && !chatRoom.matchPassword(password)) { | ||
log.warn("채팅방 비밀번호가 일치하지 않습니다. chatRoomId: {}", chatRoomId); | ||
throw new ChatRoomErrorException(ChatRoomErrorCode.INVALID_PASSWORD); | ||
} | ||
|
||
User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND)); | ||
ChatMember member = chatMemberService.createMember(user, chatRoom); | ||
|
||
eventPublisher.publishEvent(ChatRoomJoinEvent.of(chatRoomId, member.getName())); | ||
|
||
return Pair.of(chatRoom, currentMemberCount.intValue() + 1); | ||
} | ||
|
||
private boolean isFullRoom(Long currentMemberCount) { | ||
return currentMemberCount >= MAX_MEMBER_COUNT; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
...pp-external-api/src/main/java/kr/co/pennyway/api/apis/chat/usecase/ChatMemberUseCase.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package kr.co.pennyway.api.apis.chat.usecase; | ||
|
||
import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; | ||
import kr.co.pennyway.api.apis.chat.mapper.ChatRoomMapper; | ||
import kr.co.pennyway.api.apis.chat.service.ChatMemberJoinService; | ||
import kr.co.pennyway.common.annotation.UseCase; | ||
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.tuple.Pair; | ||
|
||
@Slf4j | ||
@UseCase | ||
@RequiredArgsConstructor | ||
public class ChatMemberUseCase { | ||
private final ChatMemberJoinService chatMemberJoinService; | ||
|
||
public ChatRoomRes.Detail joinChatRoom(Long userId, Long chatRoomId, Integer password) { | ||
Pair<ChatRoom, Integer> chatRoom = chatMemberJoinService.execute(userId, chatRoomId, password); | ||
|
||
return ChatRoomMapper.toChatRoomResDetail(chatRoom.getLeft(), false, chatRoom.getRight()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.