diff --git a/backend/chatroom-service/build.gradle b/backend/chatroom-service/build.gradle index 8a58511..4a5d82f 100644 --- a/backend/chatroom-service/build.gradle +++ b/backend/chatroom-service/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.2.2' + id 'org.springframework.boot' version '3.2.1' id 'io.spring.dependency-management' version '1.1.4' } @@ -32,8 +32,9 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'com.mysql:mysql-connector-j' -// runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' +// runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/dto/response/EnterChatMemberResponse.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/dto/response/EnterChatMemberResponse.java new file mode 100644 index 0000000..821f6db --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/dto/response/EnterChatMemberResponse.java @@ -0,0 +1,28 @@ +package com.tadak.chatroomservice.domain.chatmember.dto.response; + +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMember; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EnterChatMemberResponse { + + private Long chatMemberId; + private Long chatRoomId; + private String username; + private Integer participation; + + public static EnterChatMemberResponse of(ChatMember chatMember, Integer participation){ + return EnterChatMemberResponse.builder() + .chatMemberId(chatMember.getId()) + .chatRoomId(chatMember.getChatRoom().getId()) + .username(chatMember.getUsername()) + .participation(participation) + .build(); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/entity/ChatMember.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/entity/ChatMember.java new file mode 100644 index 0000000..e5123ab --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/entity/ChatMember.java @@ -0,0 +1,39 @@ +package com.tadak.chatroomservice.domain.chatmember.entity; + +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import static com.tadak.chatroomservice.domain.chatmember.entity.ChatMemberType.*; +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +@SuperBuilder +@EntityListeners(AuditingEntityListener.class) +public class ChatMember { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = LAZY) + private ChatRoom chatRoom; + + private String username; + + @Builder.Default + @Enumerated(STRING) + private ChatMemberType type = IN_ROOM; + + public void updateState() { + this.type = KICKED; + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/entity/ChatMemberType.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/entity/ChatMemberType.java new file mode 100644 index 0000000..91d319e --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/entity/ChatMemberType.java @@ -0,0 +1,5 @@ +package com.tadak.chatroomservice.domain.chatmember.entity; + +public enum ChatMemberType { + IN_ROOM, KICKED, EXIT +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/repository/ChatMemberRepository.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/repository/ChatMemberRepository.java new file mode 100644 index 0000000..c387a7d --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/repository/ChatMemberRepository.java @@ -0,0 +1,15 @@ +package com.tadak.chatroomservice.domain.chatmember.repository; + +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMember; +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ChatMemberRepository extends JpaRepository { + Optional findByChatRoomAndUsername(ChatRoom chatRoom, String username); + + boolean existsByChatRoomAndUsername(ChatRoom chatRoom, String username); +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/service/ChatMemberService.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/service/ChatMemberService.java new file mode 100644 index 0000000..8cc7fd0 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatmember/service/ChatMemberService.java @@ -0,0 +1,76 @@ +package com.tadak.chatroomservice.domain.chatmember.service; + +import com.tadak.chatroomservice.domain.chatmember.dto.response.EnterChatMemberResponse; +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMember; +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMemberType; +import com.tadak.chatroomservice.domain.chatmember.repository.ChatMemberRepository; +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import com.tadak.chatroomservice.domain.chatroom.exception.AlreadyKickedException; +import com.tadak.chatroomservice.domain.chatroom.exception.CannotTransferOwnershipException; +import com.tadak.chatroomservice.domain.chatroom.exception.NotFoundChatMemberException; +import com.tadak.chatroomservice.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatMemberService { + + private final ChatMemberRepository chatMemberRepository; + + @Transactional + public EnterChatMemberResponse enterMember(ChatRoom chatRoom, String username) { + ChatMember chatMember = ChatMember.builder() + .chatRoom(chatRoom) + .username(username) + .build(); + + chatMemberRepository.save(chatMember); + // 채팅방 인원 증가 + chatRoom.increaseParticipation(); + + return EnterChatMemberResponse.of(chatMember, chatRoom.getParticipation()); + } + + public ChatMember findByChatMember(Long chatMemberId) { + return chatMemberRepository.findById(chatMemberId) + .orElseThrow(() -> new NotFoundChatMemberException(ErrorCode.NOT_FOUND_CHAT_MEMBER_ERROR)); + } + + public boolean validEnterChatMember(ChatRoom chatRoom, String username) { + + ChatMember chatMember = chatMemberRepository.findByChatRoomAndUsername(chatRoom, username) + .orElse(null); + + if (chatMember == null){ + return false; + } + + log.info("chatMember type = {}", chatMember.getType()); + + return chatMember.getType() == ChatMemberType.KICKED; + } + + // 존재하면 true, 존재하지 않으면 false + public boolean existsChatRoomAndUsername(ChatRoom chatRoom, String username) { + return chatMemberRepository.existsByChatRoomAndUsername(chatRoom, username); + } + + // ChatMember 가지고 오기 + public ChatMember getChatMemberByChatRoomAndUsername(ChatRoom chatRoom, String username) { + ChatMember chatMember = chatMemberRepository.findByChatRoomAndUsername(chatRoom, username) + .orElseThrow(() -> new NotFoundChatMemberException(ErrorCode.NOT_FOUND_CHAT_MEMBER_ERROR)); + + // 방장 위임 할 경우 exception + if (chatMember.getType() == ChatMemberType.KICKED){ + throw new CannotTransferOwnershipException(ErrorCode.CANNOT_TRANSFER_OWNER_ERROR); + } + + return chatMember; + } + +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/controller/ChatRoomController.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/controller/ChatRoomController.java new file mode 100644 index 0000000..dc8cf82 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/controller/ChatRoomController.java @@ -0,0 +1,83 @@ +package com.tadak.chatroomservice.domain.chatroom.controller; + +import com.tadak.chatroomservice.domain.chatmember.dto.response.EnterChatMemberResponse; +import com.tadak.chatroomservice.domain.chatroom.dto.request.ChatRoomRequest; +import com.tadak.chatroomservice.domain.chatroom.dto.response.ChangeOwnerResponse; +import com.tadak.chatroomservice.domain.chatroom.dto.response.ChatRoomResponse; +import com.tadak.chatroomservice.domain.chatroom.dto.response.KickMemberResponse; +import com.tadak.chatroomservice.domain.chatroom.service.ChatRoomService; +import com.tadak.chatroomservice.domain.chatroom.dto.request.CreateChatroomRequest; +import com.tadak.chatroomservice.domain.chatroom.dto.response.CreateChatroomResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chatroom-service") +@Slf4j +public class ChatRoomController { + + private final ChatRoomService chatRoomService; + + /** + * 방 생성 + */ + @PostMapping("/create") + public ResponseEntity create(@RequestBody CreateChatroomRequest chatroomRequest){ + CreateChatroomResponse createChatroomResponse = chatRoomService.create(chatroomRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(createChatroomResponse); + } + + /** + * 방 입장 + */ + @PostMapping("/room-in/{roomId}") + public ResponseEntity enter(@PathVariable Long roomId, @RequestBody ChatRoomRequest chatRoomRequest){ + EnterChatMemberResponse enter = chatRoomService.enter(roomId, chatRoomRequest); + return ResponseEntity.status(HttpStatus.OK).body(enter); + } + + /** + * 방 삭제 + */ + @DeleteMapping("/delete/{roomId}") + public ResponseEntity deleteChatRoom(@PathVariable Long roomId, @RequestBody ChatRoomRequest chatRoomRequest) { + chatRoomService.deleteChatRoom(roomId, chatRoomRequest.getUsername()); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + + /** + * 방 전체 리스트 조회 + */ + @GetMapping("/rooms") + public ResponseEntity> getAllChatRoom() { + List chatRooms = chatRoomService.findAll(); + return ResponseEntity.status(HttpStatus.OK).body(chatRooms); + } + + /** + * 강퇴 + */ + @PostMapping("/rooms/{roomId}/kicked/{chatMemberId}") + public ResponseEntity kickedMember(@PathVariable Long roomId, @PathVariable Long chatMemberId, + @RequestBody ChatRoomRequest chatRoomRequest){ + KickMemberResponse kickMemberResponse = chatRoomService.kickMember(roomId, chatMemberId, chatRoomRequest.getUsername()); + return ResponseEntity.status(HttpStatus.OK).body(kickMemberResponse); + } + + /** + * 방장 위임 + */ + @PatchMapping("/rooms/{roomId}/change-owner/{username}") + public ResponseEntity changeOwner(@PathVariable Long roomId, @PathVariable String username, + @RequestBody ChatRoomRequest chatRoomRequest){ + ChangeOwnerResponse changeOwnerResponse = chatRoomService.changeOwner(roomId, username, chatRoomRequest.getUsername()); + return ResponseEntity.status(HttpStatus.OK).body(changeOwnerResponse); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/request/ChatRoomRequest.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/request/ChatRoomRequest.java new file mode 100644 index 0000000..e411992 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/request/ChatRoomRequest.java @@ -0,0 +1,15 @@ +package com.tadak.chatroomservice.domain.chatroom.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ChatRoomRequest { + + private String username; +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/request/CreateChatroomRequest.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/request/CreateChatroomRequest.java new file mode 100644 index 0000000..afa1bbf --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/request/CreateChatroomRequest.java @@ -0,0 +1,19 @@ +package com.tadak.chatroomservice.domain.chatroom.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CreateChatroomRequest { + + private String roomName; + private String description; + private String owner; + private String category; + private Integer capacity; +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/ChangeOwnerResponse.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/ChangeOwnerResponse.java new file mode 100644 index 0000000..693d0b4 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/ChangeOwnerResponse.java @@ -0,0 +1,24 @@ +package com.tadak.chatroomservice.domain.chatroom.dto.response; + +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChangeOwnerResponse { + + private Long chatRoomId; + private String newOwner; + + public static ChangeOwnerResponse from(ChatRoom chatRoom){ + return ChangeOwnerResponse.builder() + .chatRoomId(chatRoom.getId()) + .newOwner(chatRoom.getOwner()) + .build(); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/ChatRoomResponse.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/ChatRoomResponse.java new file mode 100644 index 0000000..877003d --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/ChatRoomResponse.java @@ -0,0 +1,32 @@ +package com.tadak.chatroomservice.domain.chatroom.dto.response; + +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChatRoomResponse { + + private Long roomId; + private String roomName; + private String description; + private Integer participation; + private Integer capacity; + private String owner; + + public static ChatRoomResponse from(ChatRoom chatRoom) { + return ChatRoomResponse.builder() + .roomId(chatRoom.getId()) + .roomName(chatRoom.getRoomName()) + .description(chatRoom.getDescription()) + .participation(chatRoom.getParticipation()) + .capacity(chatRoom.getCapacity()) + .owner(chatRoom.getOwner()) + .build(); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/CreateChatroomResponse.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/CreateChatroomResponse.java new file mode 100644 index 0000000..1d985e5 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/CreateChatroomResponse.java @@ -0,0 +1,36 @@ +package com.tadak.chatroomservice.domain.chatroom.dto.response; + +import com.tadak.chatroomservice.domain.chatmember.dto.response.EnterChatMemberResponse; +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class CreateChatroomResponse { + + private Long id; + private String roomName; + private String description; + private String owner; + private String category; + private Integer participation; + private Integer capacity; + private EnterChatMemberResponse chatMemberResponse; + + public static CreateChatroomResponse of(ChatRoom chatRoom, EnterChatMemberResponse chatMemberResponse) { + + return CreateChatroomResponse.builder() + .id(chatRoom.getId()) + .roomName(chatRoom.getRoomName()) + .description(chatRoom.getDescription()) + .owner(chatRoom.getOwner()) + .category(chatRoom.getCategory()) + .participation(chatRoom.getParticipation()) + .capacity(chatRoom.getCapacity()) + .chatMemberResponse(chatMemberResponse) + .build(); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/KickMemberResponse.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/KickMemberResponse.java new file mode 100644 index 0000000..7028592 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/dto/response/KickMemberResponse.java @@ -0,0 +1,30 @@ +package com.tadak.chatroomservice.domain.chatroom.dto.response; + +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMember; +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMemberType; +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class KickMemberResponse { + + private String username; + private ChatMemberType type; + private Long chatRoomId; + private Integer participation; + + public static KickMemberResponse of(ChatMember chatMember, ChatRoom chatRoom){ + return KickMemberResponse.builder() + .username(chatMember.getUsername()) + .type(chatMember.getType()) + .chatRoomId(chatRoom.getId()) + .participation(chatRoom.getParticipation()) + .build(); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/entity/ChatRoom.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/entity/ChatRoom.java new file mode 100644 index 0000000..d7cfd2f --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/entity/ChatRoom.java @@ -0,0 +1,78 @@ +package com.tadak.chatroomservice.domain.chatroom.entity; + +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMember; +import com.tadak.chatroomservice.domain.chatroom.dto.request.CreateChatroomRequest; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@SuperBuilder +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class ChatRoom { + + private static Integer DEFAULT_PARTICIPATION = 0; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @NotNull + @Size(max = 30, message = "방 제목은 30자를 초과할 수 없습니다.") + private String roomName; + @NotNull + @Size(max = 255, message = "방 설명은 255자를 넘길 수 없습니다.") + private String description; + @NotNull + private String owner; + @NotNull + @Size(max = 10, message = "카테고리 글자는 10자를 넘길 수 없습니다.") + private String category; + @NotNull + private Integer participation; + @NotNull + private Integer capacity; + + @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List chatMembers = new ArrayList<>(); + + @CreatedDate + private LocalDateTime createdAt; + @LastModifiedDate + private LocalDateTime modifiedAt; + + + public static ChatRoom toEntity(CreateChatroomRequest chatroomRequest){ + return ChatRoom.builder() + .roomName(chatroomRequest.getRoomName()) + .description(chatroomRequest.getDescription()) + .owner(chatroomRequest.getOwner()) + .category(chatroomRequest.getCategory()) + .participation(DEFAULT_PARTICIPATION) + .capacity(chatroomRequest.getCapacity()) + .build(); + } + + public void increaseParticipation() { + this.participation++; + } + + public void decreaseParticipation() { + this.participation--; + } + + public void updateOwner(String username) { + this.owner = username; + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/AlreadyKickedException.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/AlreadyKickedException.java new file mode 100644 index 0000000..6d1c595 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/AlreadyKickedException.java @@ -0,0 +1,11 @@ +package com.tadak.chatroomservice.domain.chatroom.exception; + +import com.tadak.chatroomservice.global.error.ErrorCode; +import com.tadak.chatroomservice.global.error.common.BusinessException; + +public class AlreadyKickedException extends BusinessException { + + public AlreadyKickedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/CannotTransferOwnershipException.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/CannotTransferOwnershipException.java new file mode 100644 index 0000000..a6929ca --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/CannotTransferOwnershipException.java @@ -0,0 +1,10 @@ +package com.tadak.chatroomservice.domain.chatroom.exception; + +import com.tadak.chatroomservice.global.error.ErrorCode; +import com.tadak.chatroomservice.global.error.common.BusinessException; + +public class CannotTransferOwnershipException extends BusinessException { + public CannotTransferOwnershipException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotFoundChatMemberException.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotFoundChatMemberException.java new file mode 100644 index 0000000..636837d --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotFoundChatMemberException.java @@ -0,0 +1,10 @@ +package com.tadak.chatroomservice.domain.chatroom.exception; + +import com.tadak.chatroomservice.global.error.ErrorCode; +import com.tadak.chatroomservice.global.error.common.BusinessException; + +public class NotFoundChatMemberException extends BusinessException { + public NotFoundChatMemberException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotFoundChatRoomException.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotFoundChatRoomException.java new file mode 100644 index 0000000..4a5a4ee --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotFoundChatRoomException.java @@ -0,0 +1,11 @@ +package com.tadak.chatroomservice.domain.chatroom.exception; + +import com.tadak.chatroomservice.global.error.ErrorCode; +import com.tadak.chatroomservice.global.error.common.BusinessException; + +public class NotFoundChatRoomException extends BusinessException { + + public NotFoundChatRoomException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotRoomOwnerException.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotRoomOwnerException.java new file mode 100644 index 0000000..aaa8d48 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/exception/NotRoomOwnerException.java @@ -0,0 +1,11 @@ +package com.tadak.chatroomservice.domain.chatroom.exception; + +import com.tadak.chatroomservice.global.error.ErrorCode; +import com.tadak.chatroomservice.global.error.common.BusinessException; + +public class NotRoomOwnerException extends BusinessException { + + public NotRoomOwnerException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/repository/ChatRoomRepository.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/repository/ChatRoomRepository.java new file mode 100644 index 0000000..3fb59ac --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/repository/ChatRoomRepository.java @@ -0,0 +1,10 @@ +package com.tadak.chatroomservice.domain.chatroom.repository; + +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ChatRoomRepository extends JpaRepository { + +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/service/ChatRoomService.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/service/ChatRoomService.java new file mode 100644 index 0000000..59f7ab6 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/domain/chatroom/service/ChatRoomService.java @@ -0,0 +1,130 @@ +package com.tadak.chatroomservice.domain.chatroom.service; + +import com.tadak.chatroomservice.domain.chatmember.dto.response.EnterChatMemberResponse; +import com.tadak.chatroomservice.domain.chatmember.entity.ChatMember; +import com.tadak.chatroomservice.domain.chatmember.service.ChatMemberService; +import com.tadak.chatroomservice.domain.chatroom.dto.request.ChatRoomRequest; +import com.tadak.chatroomservice.domain.chatroom.dto.response.ChangeOwnerResponse; +import com.tadak.chatroomservice.domain.chatroom.dto.response.ChatRoomResponse; +import com.tadak.chatroomservice.domain.chatroom.dto.response.KickMemberResponse; +import com.tadak.chatroomservice.domain.chatroom.exception.AlreadyKickedException; +import com.tadak.chatroomservice.domain.chatroom.exception.NotFoundChatRoomException; +import com.tadak.chatroomservice.domain.chatroom.exception.NotRoomOwnerException; +import com.tadak.chatroomservice.domain.chatroom.repository.ChatRoomRepository; +import com.tadak.chatroomservice.domain.chatroom.dto.request.CreateChatroomRequest; +import com.tadak.chatroomservice.domain.chatroom.dto.response.CreateChatroomResponse; +import com.tadak.chatroomservice.domain.chatroom.entity.ChatRoom; +import com.tadak.chatroomservice.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class ChatRoomService { + + private final ChatRoomRepository chatRoomRepository; + private final ChatMemberService chatMemberService; + + @Transactional + public CreateChatroomResponse create(CreateChatroomRequest chatroomRequest) { + ChatRoom chatRoom = ChatRoom.toEntity(chatroomRequest); + // 방 생성 + chatRoomRepository.save(chatRoom); + // 방 입장 + EnterChatMemberResponse enterChatMemberResponse = chatMemberService.enterMember(chatRoom, chatRoom.getOwner()); + + return CreateChatroomResponse.of(chatRoom, enterChatMemberResponse); + + } + + @Transactional + public EnterChatMemberResponse enter(Long roomId, ChatRoomRequest chatRoomRequest) { + ChatRoom chatRoom = findByChatRoom(roomId); + + if (chatMemberService.validEnterChatMember(chatRoom, chatRoomRequest.getUsername())){ + throw new AlreadyKickedException(ErrorCode.KICKED_MEMBER_ERROR); + } + + // 채팅방에 없는 member 일 경우 save 로직 + if (!chatMemberService.existsChatRoomAndUsername(chatRoom, chatRoomRequest.getUsername())) { + return chatMemberService.enterMember(chatRoom, chatRoomRequest.getUsername()); + } + + // 채팅방에 member가 있을 경우 get으로 가져와서 전달 + ChatMember existingChatMember = chatMemberService.getChatMemberByChatRoomAndUsername(chatRoom, chatRoomRequest.getUsername()); + return EnterChatMemberResponse.of(existingChatMember, chatRoom.getParticipation()); + } + + public List findAll() { + List chatRooms = chatRoomRepository.findAll(); + + return chatRooms.stream() + .map(ChatRoomResponse::from) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteChatRoom(Long roomId, String username) { + ChatRoom chatRoom = findByChatRoom(roomId); + + validOwner(username, chatRoom.getOwner()); + + chatRoomRepository.delete(chatRoom); + } + + @Transactional + public KickMemberResponse kickMember(Long roomId, Long chatMemberId, String username) { + ChatRoom chatRoom = findByChatRoom(roomId); + ChatMember chatMember = chatMemberService.findByChatMember(chatMemberId); + // 방장 검증 + validOwner(username, chatRoom.getOwner()); + + // 상태를 KICKED로 변경 + chatMember.updateState(); + // 채팅방 인원 감소 + chatRoom.decreaseParticipation(); + + return KickMemberResponse.of(chatMember, chatRoom); + } + + /** + * 방장 검증 + */ + private void validOwner(String owner, String chatRoomOwner) { + if (!owner.equals(chatRoomOwner)){ + throw new NotRoomOwnerException(ErrorCode.NOT_OWNER_ERROR); + } + } + + /** + * 방 찾기 + */ + private ChatRoom findByChatRoom(Long roomId) { + return chatRoomRepository.findById(roomId) + .orElseThrow(() -> new NotFoundChatRoomException(ErrorCode.NOT_FOUND_CHATROOM_ERROR)); + } + + /** + * @param roomId : 방 ID + * @param username : Owner가 될 대상 + * @param owner : 현재 Owner + */ + @Transactional + public ChangeOwnerResponse changeOwner(Long roomId, String username, String owner) { + ChatRoom chatRoom = findByChatRoom(roomId); + validOwner(owner, chatRoom.getOwner()); + + chatMemberService.getChatMemberByChatRoomAndUsername(chatRoom, username); + + chatRoom.updateOwner(username); + + return ChangeOwnerResponse.from(chatRoom); + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/ErrorCode.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/ErrorCode.java new file mode 100644 index 0000000..719cc53 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/ErrorCode.java @@ -0,0 +1,43 @@ +package com.tadak.chatroomservice.global.error; + +import lombok.Getter; + +@Getter +public enum ErrorCode { + + BAD_REQUEST_ERROR(400, "G001", "Bad Request Exception"), + REQUEST_BODY_MISSING_ERROR(400, "G002", "Required request body is missing"), + MISSING_REQUEST_PARAMETER_ERROR(400, "G004", "Missing Servlet RequestParameter Exception"), + NOT_FOUND_ERROR(404, "G009", "Not Found Exception"), + NULL_POINT_ERROR(404, "G010", "Null Point Exception"), + NOT_VALID_ERROR(404, "G011", "handle Validation Exception"), + INTERNAL_SERVER_ERROR(500, "G999", "Internal Server Error Exception"), + + /** + * 1300 ~ 1399 (ChatRoom error) + */ + NOT_FOUND_CHATROOM_ERROR(1300, "G1300", "현재 존재하지 않는 채팅 방 입니다."), + NOT_OWNER_ERROR(1301, "G1300", "방장 권한이 없습니다."), + KICKED_MEMBER_ERROR(1302, "G1300", "현재 강퇴당한 채팅 방 입니다."), + CANNOT_TRANSFER_OWNER_ERROR(1303, "G1300", "해당 유저는 강퇴당한 유저이기 떄문에 방장 위임을 할 수 없습니다."), + + /** + * 1400 ~ 1499 + */ + INVALID_INPUT_VALUE(1400, "G1400", "잘못된 입력 값 입니다."), + + /** + * 1500 ~ 1599 (ChatMember error) + */ + NOT_FOUND_CHAT_MEMBER_ERROR(1500, "G1500", "현재 채팅방에 해당 member가 존재하지 않습니다."); + + private final int status; + private final String code; + private final String message; + + ErrorCode(final int status, final String code, final String message) { + this.status = status; + this.code = code; + this.message = message; + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/ErrorResponse.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/ErrorResponse.java new file mode 100644 index 0000000..d50ab3f --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/ErrorResponse.java @@ -0,0 +1,86 @@ +package com.tadak.chatroomservice.global.error; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.validation.BindingResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ErrorResponse { + + private int status; // 에러 상태 코드 + private String code; // 에러 구분 코드 + private String message; // 에러 메시지 + + @Builder + protected ErrorResponse(final ErrorCode code) { + this.message = code.getMessage(); + this.status = code.getStatus(); + this.code = code.getCode(); + } + + @Builder + protected ErrorResponse(final ErrorCode code, final String reason) { + this.message = code.getMessage(); + this.status = code.getStatus(); + this.code = code.getCode(); + } + + @Builder + protected ErrorResponse(final ErrorCode code, final List errors) { + this.message = code.getMessage(); + this.status = code.getStatus(); + this.code = code.getCode(); + } + + public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) { + return new ErrorResponse(code, FieldError.of(bindingResult)); + } + + public static ErrorResponse of(final ErrorCode code) { + return new ErrorResponse(code); + } + + public static ErrorResponse of(final ErrorCode code, final String reason) { + return new ErrorResponse(code, reason); + } + + /** + * e.getBindingResult() 형태로 전달받는 error 처리하여 변경 + */ + @Getter + public static class FieldError { + private final String field; + private final String value; + private final String reason; + + public static List of(final String field, final String value, final String reason) { + List fieldErrors = new ArrayList<>(); + fieldErrors.add(new FieldError(field, value, reason)); + return fieldErrors; + } + + private static List of(final BindingResult bindingResult) { + final List fieldErrors = bindingResult.getFieldErrors(); + return fieldErrors.stream() + .map(error -> new FieldError( + error.getField(), + error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(), + error.getDefaultMessage())) + .collect(Collectors.toList()); + } + + @Builder + FieldError(String field, String value, String reason) { + this.field = field; + this.value = value; + this.reason = reason; + } + } +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/GlobalExceptionHandler.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/GlobalExceptionHandler.java new file mode 100644 index 0000000..099c0d4 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/GlobalExceptionHandler.java @@ -0,0 +1,122 @@ +package com.tadak.chatroomservice.global.error; + +import com.tadak.chatroomservice.global.error.common.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.servlet.NoHandlerFoundException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + private final HttpStatus HTTP_STATUS_OK = HttpStatus.OK; + + /** + * API 호출 시 객체 혹은 파라미터의 값이 제대로 되지 않을 경우 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + log.error("handleMethodArgumentNotValidException", ex); + BindingResult bindingResult = ex.getBindingResult(); + StringBuilder stringBuilder = new StringBuilder(); + for (FieldError fieldError : bindingResult.getFieldErrors()) { + stringBuilder.append(fieldError.getField()).append(":"); + stringBuilder.append(fieldError.getDefaultMessage()); + stringBuilder.append(", "); + } + final ErrorResponse response = ErrorResponse.of(ErrorCode.NOT_VALID_ERROR, String.valueOf(stringBuilder)); + return new ResponseEntity<>(response, HTTP_STATUS_OK); + } + + /** + * Body 부분에 객체 데이터가 넘어 오지 않았을 경우 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + protected ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + log.error("HttpMessageNotReadableException", ex); + final ErrorResponse response = ErrorResponse.of(ErrorCode.REQUEST_BODY_MISSING_ERROR, ex.getMessage()); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + /** + * 클라이언트에서 body로 객체 데이터가 넘어오지 않을 경우 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + protected ResponseEntity handleMissingRequestHeaderExceptionException(MissingServletRequestParameterException ex) { + log.error("handleMissingServletRequestParameterException", ex); + final ErrorResponse response = ErrorResponse.of(ErrorCode.MISSING_REQUEST_PARAMETER_ERROR, ex.getMessage()); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + /** + * 잘못된 서버 요청일 경우 + */ + @ExceptionHandler(HttpClientErrorException.BadRequest.class) + protected ResponseEntity handleBadRequestException(HttpClientErrorException e) { + log.error("HttpClientErrorException.BadRequest", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.BAD_REQUEST_ERROR, e.getMessage()); + return new ResponseEntity<>(response, HTTP_STATUS_OK); + } + + /** + * 잘못된 주소로 요청한 경우 + */ + @ExceptionHandler(NoHandlerFoundException.class) + protected ResponseEntity handleNoHandlerFoundExceptionException(NoHandlerFoundException e) { + log.error("handleNoHandlerFoundExceptionException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.NOT_FOUND_ERROR, e.getMessage()); + return new ResponseEntity<>(response, HTTP_STATUS_OK); + } + + /** + * null 값이 발생한 경우 + */ + @ExceptionHandler(NullPointerException.class) + protected ResponseEntity handleNullPointerException(NullPointerException e) { + log.error("handleNullPointerException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.NULL_POINT_ERROR, e.getMessage()); + return new ResponseEntity<>(response, HTTP_STATUS_OK); + } + + /** + * exception이 발생한 경우 + */ + @ExceptionHandler(Exception.class) + protected final ResponseEntity handleAllExceptions(Exception ex) { + log.error("Exception", ex); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); + return new ResponseEntity<>(response, HTTP_STATUS_OK); + } + + /** + * Custom Exception + */ + @ExceptionHandler(BusinessException.class) + protected ResponseEntity handleBusinessException(final BusinessException ex) { + log.error("handleBusinessException", ex); + final ErrorCode errorCode = ex.getErrorCode(); + final ErrorResponse response = ErrorResponse.of(errorCode); + return new ResponseEntity<>(response, HTTP_STATUS_OK); + } + + /** + * binding Exception + */ + @ExceptionHandler(BindException.class) + protected ResponseEntity handleBindException(BindException ex) { + log.error("handleBindException", ex); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, ex.getBindingResult()); + return new ResponseEntity<>(response, HTTP_STATUS_OK); + } + +} diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/common/BusinessException.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/common/BusinessException.java new file mode 100644 index 0000000..0a82230 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/error/common/BusinessException.java @@ -0,0 +1,22 @@ +package com.tadak.chatroomservice.global.error.common; + +import com.tadak.chatroomservice.global.error.ErrorCode; + +public class BusinessException extends RuntimeException{ + + private ErrorCode errorCode; + + public BusinessException(String message, ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/p6spy/P6SpySqlFormatter.java b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/p6spy/P6SpySqlFormatter.java new file mode 100644 index 0000000..6180e82 --- /dev/null +++ b/backend/chatroom-service/src/main/java/com/tadak/chatroomservice/global/p6spy/P6SpySqlFormatter.java @@ -0,0 +1,42 @@ +package com.tadak.chatroomservice.global.p6spy; + +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.spy.P6SpyOptions; +import com.p6spy.engine.spy.appender.MessageFormattingStrategy; +import jakarta.annotation.PostConstruct; +import org.hibernate.engine.jdbc.internal.FormatStyle; +import org.springframework.context.annotation.Configuration; + +import java.util.Locale; + +@Configuration +public class P6SpySqlFormatter implements MessageFormattingStrategy { + + @PostConstruct + public void setLogMessageFormat() { + P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName()); + } + + @Override + public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) { + sql = formatSql(category, sql); + return String.format("[%s] | %d ms | %s", category, elapsed, highlight(formatSql(category, sql))); + } + + private String formatSql(String category, String sql) { + if (sql != null && !sql.trim().isEmpty() && Category.STATEMENT.getName().equals(category)) { + String trimmedSQL = sql.trim().toLowerCase(Locale.ROOT); + if (trimmedSQL.startsWith("create") || trimmedSQL.startsWith("alter") || trimmedSQL.startsWith("comment")) { + sql = FormatStyle.DDL.getFormatter().format(sql); + } else { + sql = FormatStyle.BASIC.getFormatter().format(sql); + } + return sql; + } + return sql; + } + + private String highlight(String sql) { + return FormatStyle.HIGHLIGHT.getFormatter().format(sql); + } +}