Skip to content

Commit

Permalink
MATE-79 : [FEAT] 메이트 채팅방 기능 구현 (#76)
Browse files Browse the repository at this point in the history
* MATE-79 : [FEAT] 메이트 채팅방 컨트롤러 구현

* MATE-79 : [FEAT] 메이트 채팅방 서비스 구현

* MATE-79 : [FEAT] 메이트 채팅방 리포지토리 구현

* MATE-79 : [FEAT] 메이트 채팅방 요청 DTO 구현

* MATE-79 : [FEAT] 메이트 채팅방 반환 DTO 구현

* MATE-79 : [FEAT] 메이트 채팅방 엔티티 수정

* MATE-79 : [FEAT] 메이트 채팅방 에러코드 추가

* MATE-79 : [FEAT] 메이트 채팅방 MessageType 구현

* MATE-79 : [FEAT] 메이트 채팅방 테스트용 html 파일 구현

* MATE-79 : [FEAT] 메이트 채팅 목록 반환 컨트롤러 구현

* MATE-79 : [FEAT] 메이트 채팅 목록 반환 서비스 구현

* MATE-79 : [FEAT] 메이트 채팅 목록 반환 리포지토리 구현

* MATE-79 : [FEAT] 메이트 채팅 목록 반환 리포지토리 구현

* MATE-79 : [FEAT] 메이트 채팅 목록 반환 DTO 반환

* MATE-79 : [FEAT] MateChatRoom 엔티티 변환

* MATE-79 : [CHORE] MateChatRoomController 주석 설명 수정

* MATE-79 : [FEAT] 채팅방 반환값 수정

* MATE-79 : [FEAT] 메이트 채팅방 관련 불필요한 기능 제거

* MATE-79 : [FEAT] 메이트 채팅방 반환 DTO 값 추가

* MATE-79 : [FEAT] 메이트 채팅방 요청, 반환 DTO 값 수정

* MATE-79 : [Chore] 메이트 채팅방 import문 수정 및 사용하지 않는 메서드 정리

* MATE-79 : [FEAT] 메이트 채팅방 입장 및 조회 기능 수정

* MATE-79 : [FEAT] 메이트 채팅방 입장 및 조회 기능 수정

* MATE-79 : [CHORE] 메이트 채팅방 입장 및 조회 기능 Swagger 적용

* MATE-79 : [FEAT] 메이트 채팅방 컨트롤러 구현

* MATE-79 : [FEAT] 메이트 채팅방 생성/입장 서비스 구현

* MATE-79 : [FEAT] 메이트 채팅방 메세지 조회 서비스 구현

* MATE-79 : [FEAT] 메이트 채팅방 목록 조회 서비스 구현

* MATE-79 : [FEAT] 메이트 채팅방 컨트롤러 url 수정

* MATE-79 : [FEAT] 메이트 채팅방 메세지 컨트롤러 구현

* MATE-79 : [FEAT] 메이트 채팅방 사용자 엔티티 구현

* MATE-79 : [FEAT] 메이트 채팅방 엔티티 구현

* MATE-79 : [FEAT] 메이트 채팅방 메세지 엔티티 구현

* MATE-79 : [FEAT] 메이트 채팅방 반환 DTO 구현

* MATE-79 : [FEAT] 메이트 채팅방 목록 반환 DTO 구현

* MATE-79 : [FEAT] 메이트 채팅방 메시지 페이지 반환 DTO 구현

* MATE-79 : [FEAT] 메이트 채팅방 메세지 요청 DTO 구현

* MATE-79 : [FEAT] 메이트 채팅방 관련 리포지토리 구현

* MATE-79 : [FEAT] 메이트 채팅방 멤버 관련 리포지토리 구현

* MATE-79 : [FEAT] 메이트 채팅방 메세지 관련 리포지토리 구현

* MATE-79 : [FEAT] 메이트 채팅방 메세지 서비스 구현

* MATE-79 : [FEAT] 메이트 채팅방 관련 에러 메세지 추가

* MATE-79 : [FEAT] 메이트 채팅방 관련 테스트 html

* MATE-79 : [FEAT] 메이트 후기 작성 예외처리 제거

* MATE-79 : [CHORE] 미사용 import문 제거
  • Loading branch information
MisaSohee authored Dec 6, 2024
1 parent 0229ad7 commit 5d037dd
Show file tree
Hide file tree
Showing 19 changed files with 1,186 additions and 42 deletions.
22 changes: 20 additions & 2 deletions src/main/java/com/example/mate/common/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public enum ErrorCode {

// Mate Review
NOT_PARTICIPANT_OR_AUTHOR(HttpStatus.FORBIDDEN, "R002", "리뷰어와 리뷰 대상자 모두 직관 참여자여야 합니다."),
SELF_REVIEW_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "R004", "자기 자신에 대한 리뷰는 작성할 수 없습니다."),
REVIEW_NOT_FOUND_BY_ID(HttpStatus.NOT_FOUND, "R005", "해당 ID의 리뷰를 찾을 수 없습니다."),

// FILE
Expand Down Expand Up @@ -122,7 +121,26 @@ public enum ErrorCode {

//Weather
WEATHER_DATA_NOT_FOUND(HttpStatus.NOT_FOUND, "W001", "날씨 데이터를 찾을 수 없습니다."),
WEATHER_API_ERROR(HttpStatus.NOT_FOUND, "W002", "날씨 API 호출 중 오류가 발생했습니다.");
WEATHER_API_ERROR(HttpStatus.NOT_FOUND, "W002", "날씨 API 호출 중 오류가 발생했습니다."),

// 채팅방 관련 에러
CHAT_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "CHAT001", "존재하지 않는 채팅방입니다."),
CHAT_ROOM_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "CHAT002", "채팅방 멤버가 아닙니다."),
CHAT_ROOM_FULL(HttpStatus.BAD_REQUEST, "CHAT003", "채팅방 인원이 가득 찼습니다. (최대 10명)"),
ALREADY_JOINED_CHAT_ROOM(HttpStatus.BAD_REQUEST, "CHAT004", "이미 참여 중인 채팅방입니다."),
CHAT_ROOM_CLOSED(HttpStatus.BAD_REQUEST, "CHAT005", "종료된 채팅방입니다."),
CHAT_ROOM_ACCESS_DENIED(HttpStatus.FORBIDDEN, "CHAT006", "직관 완료된 채팅방에는 새로운 유저가 입장할 수 없습니다."),
AUTHOR_LEAVE_NOT_ALLOWED(HttpStatus.FORBIDDEN, "CHAT007", "방장은 직관완료가 안된 채팅방에서 나갈 수 없습니다."),
CHAT_ROOM_NOT_MESSAGEABLE(HttpStatus.FORBIDDEN, "CHAT008", "메세지 전송이 불가능한 채팅방입니다."),

// 채팅 참여 제한 관련 에러
AGE_RESTRICTION_VIOLATED(HttpStatus.FORBIDDEN, "CHAT007", "연령 제한으로 입장할 수 없습니다."),
GENDER_RESTRICTION_VIOLATED(HttpStatus.FORBIDDEN, "CHAT008", "성별 제한으로 입장할 수 없습니다."),

// 채팅 기능 관련 에러
CHAT_NOT_AVAILABLE(HttpStatus.BAD_REQUEST, "CHAT009", "2명 이상의 사용자가 있어야 채팅이 가능합니다."),
MESSAGE_CONTENT_EMPTY(HttpStatus.BAD_REQUEST, "CHAT010", "메시지 내용을 입력해주세요."),
INVALID_MESSAGE_TYPE(HttpStatus.BAD_REQUEST, "CHAT011", "잘못된 메시지 타입입니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.mate.domain.mate.repository;

import com.example.mate.domain.mate.entity.Visit;
import com.example.mate.domain.mate.entity.VisitPart;
import com.example.mate.domain.mate.entity.VisitPartId;
import com.example.mate.domain.member.entity.Member;
Expand All @@ -19,4 +20,6 @@ public interface VisitPartRepository extends JpaRepository<VisitPart, VisitPartI
AND vp.member.id != :memberId
""")
List<Member> findMembersByVisitIdExcludeMember(@Param("visitId") Long visitId, @Param("memberId") Long memberId);

boolean existsByVisitAndMember(Visit visit, Member member);
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,6 @@ private void validateReviewEligibility(MatePost matePost, Member reviewer, Membe
// 리뷰어와 리뷰 대상자 모두 참여자(또는 방장) 여부 검증
validateParticipant(matePost, reviewer);
validateParticipant(matePost, reviewee);

// 자기 자신 리뷰 검증
validateSelfReview(reviewer, reviewee);
}

private void validateParticipant(MatePost matePost, Member member) {
Expand All @@ -276,12 +273,6 @@ private boolean isVisitParticipant(Visit visit, Member member) {
.anyMatch(part -> part.getMember().equals(member));
}

private void validateSelfReview(Member reviewer, Member reviewee) {
if (reviewer.equals(reviewee)) {
throw new CustomException(SELF_REVIEW_NOT_ALLOWED);
}
}

private Member findMemberById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND_BY_ID));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.mate.domain.mateChat.controller;

import com.example.mate.domain.mateChat.dto.request.MateChatMessageRequest;
import com.example.mate.domain.mateChat.service.MateChatMessageService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Controller;

@Controller
@RequiredArgsConstructor
public class MateChatMessageController {
private final MateChatMessageService mateChatMessageService;

@MessageMapping("/chat/mate/message")
public void handleMessage(@Payload @Valid MateChatMessageRequest message) {
mateChatMessageService.sendMessage(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.example.mate.domain.mateChat.controller;

import com.example.mate.common.response.ApiResponse;
import com.example.mate.common.response.PageResponse;
import com.example.mate.domain.mateChat.dto.response.MateChatMessageResponse;
import com.example.mate.domain.mateChat.dto.response.MateChatRoomListResponse;
import com.example.mate.domain.mateChat.dto.response.MateChatRoomResponse;
import com.example.mate.domain.mateChat.service.MateChatRoomService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/mates/chat")
@Tag(name = "MateChatRoom", description = "메이트 채팅방 관련 API")
public class MateChatRoomController {
private final MateChatRoomService chatRoomService;

@PostMapping("/post/{matePostId}")
@Operation(summary = "메이트 게시글 -> 채팅방 생성/입장", description = "메이트 게시글 페이지에서 채팅방으로 입장")
public ResponseEntity<ApiResponse<MateChatRoomResponse>> createOrJoinChatRoomFromPost(
@Parameter(description = "메이트 게시글 ID") @PathVariable Long matePostId,
@Parameter(description = "회원 ID (삭제 예정)") @RequestParam Long memberId
) {
MateChatRoomResponse response = chatRoomService.createOrJoinChatRoomFromPost(matePostId, memberId);
return ResponseEntity.ok(ApiResponse.success(response));
}

@PostMapping("/{chatroomId}/join")
@Operation(summary = "채팅방 목록 -> 채팅방 입장", description = "채팅 목록 페이지에서 채팅방으로 입장")
public ResponseEntity<ApiResponse<MateChatRoomResponse>> joinExistingChatRoom(
@Parameter(description = "채팅방 ID") @PathVariable Long chatroomId,
@Parameter(description = "회원 ID (삭제 예정)") @RequestParam Long memberId
) {
MateChatRoomResponse response = chatRoomService.joinExistingChatRoom(chatroomId, memberId);
return ResponseEntity.ok(ApiResponse.success(response));
}

@GetMapping("/{chatroomId}/members/{memberId}/messages")
@Operation(summary = "채팅방 메세지 조회", description = "채팅 목록 페이지 -> 채팅방 입장 시, 메시지 내역을 조회합니다.")
public ResponseEntity<ApiResponse<PageResponse<MateChatMessageResponse>>> getChatMessages(
@Parameter(description = "채팅방 ID") @PathVariable Long chatroomId,
@PathVariable Long memberId,
@Parameter(description = "페이지 정보") @PageableDefault Pageable pageable
) {
PageResponse<MateChatMessageResponse> messages = chatRoomService.getChatMessages(chatroomId, memberId, pageable);
return ResponseEntity.ok(ApiResponse.success(messages));
}

@GetMapping("/me")
@Operation(summary = "내 채팅방 목록 조회", description = "사용자의 채팅방 목록을 조회합니다.")
public ResponseEntity<ApiResponse<PageResponse<MateChatRoomListResponse>>> getMyChatRooms(
@Parameter(description = "회원 ID (삭제 예정)") @RequestParam Long memberId, // 추후 @AuthenticationPrincipal로 대체
@Parameter(description = "페이지 정보") @PageableDefault Pageable pageable
) {
PageResponse<MateChatRoomListResponse> response = chatRoomService.getMyChatRooms(memberId, pageable);
return ResponseEntity.ok(ApiResponse.success(response));
}

@DeleteMapping("{chatroomId}/leave")
@Operation(summary = "채팅방 나가기", description = "채팅방에서 퇴장합니다.")
public ResponseEntity<Void> leaveChatRoom(
@Parameter(description = "채팅방 ID") @PathVariable Long chatroomId,
@Parameter(description = "회원 ID (삭제 예정)") @RequestParam Long memberId // 추후 @AuthenticationPrincipal로 대체
) {
chatRoomService.leaveChatRoom(chatroomId, memberId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.example.mate.domain.mateChat.dto.request;

import com.example.mate.domain.mateChat.entity.MateChatMessage;
import com.example.mate.domain.mateChat.entity.MateChatRoom;
import com.example.mate.domain.mateChat.message.MessageType;
import com.example.mate.domain.member.entity.Member;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.*;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MateChatMessageRequest {
@NotNull(message = "메시지 타입은 필수입니다.")
private String type;

@NotNull(message = "채팅방 ID는 필수입니다.")
private Long roomId;

@NotNull(message = "발신자 ID는 필수입니다.")
private Long senderId;

@NotBlank(message = "메시지 내용은 필수입니다.")
private String message;

public static MateChatMessage toEntity(MateChatRoom chatRoom, MateChatMessageRequest request, Member sender) {
return MateChatMessage.builder()
.mateChatRoom(chatRoom)
.sender(sender)
.type(MessageType.valueOf(request.getType()))
.content(request.getMessage())
.sendTime(LocalDateTime.now())
.build();
}

public static MateChatMessageRequest createEnterMessage(Long roomId, Long senderId, String nickname) {
return MateChatMessageRequest.builder()
.type(MessageType.ENTER.name())
.roomId(roomId)
.senderId(senderId)
.message(nickname + "님이 들어왔습니다.")
.build();
}

public static MateChatMessageRequest createLeaveMessage(Long roomId, Long senderId, String nickname) {
return MateChatMessageRequest.builder()
.type(MessageType.LEAVE.name())
.roomId(roomId)
.senderId(senderId)
.message(nickname + "님이 나갔습니다.")
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.mate.domain.mateChat.dto.response;

import com.example.mate.domain.mateChat.entity.MateChatMessage;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class MateChatMessageResponse {
private Long messageId;
private Long roomId;
private Long senderId;
private String senderNickname;
private String message;
private String messageType;
private String senderImageUrl;
private LocalDateTime sendTime;

public static MateChatMessageResponse of(MateChatMessage message) {
return MateChatMessageResponse.builder()
.messageId(message.getId())
.roomId(message.getMateChatRoom().getId())
.senderId(message.getSender().getId())
.senderNickname(message.getSender().getNickname())
.message(message.getContent())
.messageType(message.getType().getValue())
.senderImageUrl(message.getSender().getImageUrl())
.sendTime(message.getSendTime())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.mate.domain.mateChat.dto.response;

import com.example.mate.domain.mateChat.entity.MateChatRoom;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class MateChatRoomListResponse {
private Long roomId;
private Long postId;
private String postImageUrl;
private String postTitle;
private String lastMessageContent;
private LocalDateTime lastMessageTime;
private Integer currentMembers;
private Boolean isActive;
private Boolean isMessageable;
private Boolean isAuthorLeft;
private Boolean isAuthor;

public static MateChatRoomListResponse from(MateChatRoom chatRoom, boolean isAuthor) {
return MateChatRoomListResponse.builder()
.roomId(chatRoom.getId())
.postId(chatRoom.getMatePost().getId())
.postImageUrl(chatRoom.getMatePost().getImageUrl())
.postTitle(chatRoom.getMatePost().getTitle())
.lastMessageContent(chatRoom.getLastChatContent())
.lastMessageTime(chatRoom.getLastChatSentAt())
.currentMembers(chatRoom.getCurrentMembers())
.isActive(chatRoom.getIsActive())
.isMessageable(chatRoom.getIsMessageable())
.isAuthorLeft(chatRoom.getIsAuthorLeft())
.isAuthor(isAuthor)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.mate.domain.mateChat.dto.response;

import com.example.mate.common.response.PageResponse;
import com.example.mate.domain.mateChat.entity.MateChatRoom;
import com.example.mate.domain.mateChat.entity.MateChatRoomMember;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class MateChatRoomResponse {
private Long roomId;
private Long matePostId;
private Long memberId;
private Integer currentMembers;
private Boolean isRoomActive;
private Boolean isMessageable;
private Boolean isAuthorLeft;
private Boolean isAuthor;
private PageResponse<MateChatMessageResponse> initialMessages;

public static MateChatRoomResponse from(
MateChatRoom chatRoom,
MateChatRoomMember member,
PageResponse<MateChatMessageResponse> messages) {
return MateChatRoomResponse.builder()
.roomId(chatRoom.getId())
.matePostId(chatRoom.getMatePost().getId())
.memberId(member.getMember().getId())
.currentMembers(chatRoom.getCurrentMembers())
.isRoomActive(chatRoom.getIsActive())
.isMessageable(chatRoom.getIsMessageable())
.isAuthorLeft(chatRoom.getIsAuthorLeft())
.isAuthor(chatRoom.getMatePost().getAuthor().getId().equals(member.getMember().getId()))
.initialMessages(messages)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Table(name = "mate_chat_message")
@Getter
Expand All @@ -22,7 +24,7 @@ public class MateChatMessage extends BaseTimeEntity {
private MateChatRoom mateChatRoom;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sender_id") // 시스템 메시지는 sender가 null일 수 있음
@JoinColumn(name = "sender_id")
private Member sender;

@Enumerated(EnumType.STRING)
Expand All @@ -32,11 +34,6 @@ public class MateChatMessage extends BaseTimeEntity {
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
private String content;

private MateChatMessage(MateChatRoom mateChatRoom, Member sender, MessageType type,
String content) {
this.mateChatRoom = mateChatRoom;
this.sender = sender;
this.type = type;
this.content = content;
}
@Column(name = "send_time", nullable = false)
private LocalDateTime sendTime;
}
Loading

0 comments on commit 5d037dd

Please sign in to comment.